Turn all sandbox bindings into coroutines

We can't have them as normal functions because reentrant calls into the
Ruby API don't work if you do that, i.e. calling into the Ruby API and
that calls mkxp-z's bindings and that calls back into the Ruby API.
This commit is contained in:
刘皓 2025-01-07 00:02:09 -05:00
parent c4cb891e37
commit c4d5d8af97
No known key found for this signature in database
GPG key ID: 7901753DB465B711
8 changed files with 216 additions and 119 deletions

View file

@ -35,6 +35,12 @@ if get_option('retro') == true
cmake = import('cmake') cmake = import('cmake')
boost_options = cmake.subproject_options()
boost_options.add_cmake_defines({
'CMAKE_POSITION_INDEPENDENT_CODE': true,
'BUILD_TESTING': false,
})
zlib_options = cmake.subproject_options() zlib_options = cmake.subproject_options()
zlib_options.add_cmake_defines({ zlib_options.add_cmake_defines({
'CMAKE_POSITION_INDEPENDENT_CODE': true, 'CMAKE_POSITION_INDEPENDENT_CODE': true,
@ -106,6 +112,7 @@ if get_option('retro') == true
library( library(
'retro-' + meson.project_name(), 'retro-' + meson.project_name(),
dependencies: [ dependencies: [
cmake.subproject('boost_asio', options: boost_options).dependency('boost_asio'),
cmake.subproject('zlib', options: zlib_options).dependency('zlibstatic'), cmake.subproject('zlib', options: zlib_options).dependency('zlibstatic'),
cmake.subproject('bzip2', options: bzip2_options).dependency('bz2_static'), cmake.subproject('bzip2', options: bzip2_options).dependency('bz2_static'),
cmake.subproject('liblzma', options: lzma_options).dependency('liblzma'), cmake.subproject('liblzma', options: lzma_options).dependency('liblzma'),

View file

@ -86,7 +86,7 @@ $(OUTDIR)/mkxp-retro-ruby/mkxp-retro-ruby.h $(OUTDIR)/mkxp-retro-ruby/mkxp-retro
mkdir -p $(OUTDIR)/mkxp-retro-ruby mkdir -p $(OUTDIR)/mkxp-retro-ruby
$(WASM2C) $(LIBDIR)/ruby.wasm -n ruby --num-outputs=8 -o $(OUTDIR)/mkxp-retro-ruby/mkxp-retro-ruby.c $(WASM2C) $(LIBDIR)/ruby.wasm -n ruby --num-outputs=8 -o $(OUTDIR)/mkxp-retro-ruby/mkxp-retro-ruby.c
$(LIBDIR)/ruby.wasm $(OUTDIR)/mkxp-retro-dist.zip.c &: $(DOWNLOADS)/ruby/Makefile $(LIBDIR)/ruby.wasm $(OUTDIR)/mkxp-retro-dist.zip.c &: $(DOWNLOADS)/ruby/Makefile extra-ruby-bindings.h
mkdir -p $(OUTDIR) mkdir -p $(OUTDIR)
cd $(DOWNLOADS)/ruby && $(MAKE) install DESTDIR=$(OUTDIR) cd $(DOWNLOADS)/ruby && $(MAKE) install DESTDIR=$(OUTDIR)
mv $(OUTDIR)/mkxp-retro-dist/bin/ruby $(LIBDIR)/ruby.wasm mv $(OUTDIR)/mkxp-retro-dist/bin/ruby $(LIBDIR)/ruby.wasm
@ -105,7 +105,7 @@ $(LIBDIR)/tags: $(DOWNLOADS)/ruby/.ext/include/$(TARGET)/ruby/config.h
echo '#include <ruby.h>' | $(WASI_CC) -E -I$(DOWNLOADS)/ruby/include -I$(DOWNLOADS)/ruby/.ext/include/$(TARGET) -o $(LIBDIR)/tags.c - echo '#include <ruby.h>' | $(WASI_CC) -E -I$(DOWNLOADS)/ruby/include -I$(DOWNLOADS)/ruby/.ext/include/$(TARGET) -o $(LIBDIR)/tags.c -
$(CTAGS) -R --fields=S --kinds-c=p --kinds-c++=p -o $(LIBDIR)/tags $(LIBDIR)/tags.c $(CTAGS) -R --fields=S --kinds-c=p --kinds-c++=p -o $(LIBDIR)/tags $(LIBDIR)/tags.c
$(DOWNLOADS)/ruby/Makefile $(DOWNLOADS)/ruby/.ext/include/$(TARGET)/ruby/config.h &: $(DOWNLOADS)/ruby/configure $(RUBY) extra-ruby-bindings.h $(LIBDIR)/usr/local/lib/libyaml.a $(LIBDIR)/usr/local/lib/libz.a $(LIBDIR)/usr/local/lib/libssl.a $(DOWNLOADS)/ruby/Makefile $(DOWNLOADS)/ruby/.ext/include/$(TARGET)/ruby/config.h &: $(DOWNLOADS)/ruby/configure $(RUBY) $(LIBDIR)/usr/local/lib/libyaml.a $(LIBDIR)/usr/local/lib/libz.a $(LIBDIR)/usr/local/lib/libssl.a
cd $(DOWNLOADS)/ruby && ./configure \ cd $(DOWNLOADS)/ruby && ./configure \
--prefix=/mkxp-retro-dist \ --prefix=/mkxp-retro-dist \
--host $(TARGET) \ --host $(TARGET) \

View file

@ -21,8 +21,8 @@
/* This file contains bindings that expose low-level functionality of the Ruby VM to the outside of the sandbox it's running in. They can be called from sandbox.cpp. */ /* This file contains bindings that expose low-level functionality of the Ruby VM to the outside of the sandbox it's running in. They can be called from sandbox.cpp. */
#ifndef MKXPZ_SANDBOX_EXTRA_RUBY_BINDINGS_H #ifndef SANDBOX_EXTRA_RUBY_BINDINGS_H
#define MKXPZ_SANDBOX_EXTRA_RUBY_BINDINGS_H #define SANDBOX_EXTRA_RUBY_BINDINGS_H
#include <stdbool.h> #include <stdbool.h>
#include <stdlib.h> #include <stdlib.h>
@ -31,43 +31,49 @@
#include "wasm/machine.h" #include "wasm/machine.h"
#include "wasm/setjmp.h" #include "wasm/setjmp.h"
#define MKXPZ_SANDBOX_API __attribute__((__visibility__("default"))) #define MKXP_SANDBOX_API __attribute__((__visibility__("default")))
/* This function should be called immediately after initializing the sandbox to perform initialization, before calling any other functions. */ /* This function should be called immediately after initializing the sandbox to perform initialization, before calling any other functions. */
MKXPZ_SANDBOX_API void mkxp_sandbox_init(void) { MKXP_SANDBOX_API void mkxp_sandbox_init(void) {
void __wasm_call_ctors(void); /* Defined by wasi-libc from the WASI SDK */ void __wasm_call_ctors(void); /* Defined by wasi-libc from the WASI SDK */
__wasm_call_ctors(); __wasm_call_ctors();
} }
/* This function should be called immediately before deinitializing the sandbox. */ /* This function should be called immediately before deinitializing the sandbox. */
MKXPZ_SANDBOX_API void mkxp_sandbox_deinit(void) { MKXP_SANDBOX_API void mkxp_sandbox_deinit(void) {
void __wasm_call_dtors(void); /* Defined by wasi-libc from the WASI SDK */ void __wasm_call_dtors(void); /* Defined by wasi-libc from the WASI SDK */
__wasm_call_dtors(); __wasm_call_dtors();
} }
/* Exposes the `malloc()` function. */ /* Exposes the `malloc()` function. */
MKXPZ_SANDBOX_API void *mkxp_sandbox_malloc(size_t size) { MKXP_SANDBOX_API void *mkxp_sandbox_malloc(size_t size) {
return malloc(size); return malloc(size);
} }
/* Exposes the `free()` function. */ /* Exposes the `free()` function. */
MKXPZ_SANDBOX_API void mkxp_sandbox_free(void *ptr) { MKXP_SANDBOX_API void mkxp_sandbox_free(void *ptr) {
free(ptr); free(ptr);
} }
/* Ruby's `rb_`/`ruby_` functions may return early before they're actually finished running.
* You can use `mkxp_sandbox_complete()` to check if the most recent call to a `rb_`/`ruby_` function finished.
* If `mkxp_sandbox_complete()` returns false, the `rb_`/`ruby_` function is not done executing yet and needs to be called again with the same arguments. */
MKXP_SANDBOX_API bool mkxp_sandbox_complete(void) {
extern void *rb_asyncify_unwind_buf; /* Defined in wasm/setjmp.c in Ruby source code */
return rb_asyncify_unwind_buf == NULL;
}
/* This function drives Ruby's asynchronous runtime. It's based on the `rb_wasm_rt_start()` function from wasm/runtime.c in the Ruby source code. /* This function drives Ruby's asynchronous runtime. It's based on the `rb_wasm_rt_start()` function from wasm/runtime.c in the Ruby source code.
* After calling any function that starts with `rb_` or `ruby_` other than `ruby_sysinit()`, you need to call `mkxp_sandbox_yield()`. * After calling any function that starts with `rb_` or `ruby_` other than `ruby_sysinit()`, you need to call `mkxp_sandbox_yield()`.
* If `mkxp_sandbox_yield()` returns false, you may proceed as usual. * If `mkxp_sandbox_yield()` returns false, you may proceed as usual.
* However, if it returns true, then you need to call the `rb_`/`ruby_` function again with the same arguments * However, if it returns true, then you need to call the `rb_`/`ruby_` function again with the same arguments
* and then call `mkxp_sandbox_yield()` again, and repeat until `mkxp_sandbox_yield()` returns false. */ * and then call `mkxp_sandbox_yield()` again, and repeat until `mkxp_sandbox_yield()` returns false. */
MKXPZ_SANDBOX_API bool mkxp_sandbox_yield(void) { MKXP_SANDBOX_API bool mkxp_sandbox_yield(void) {
static void (*fiber_entry_point)(void *, void *) = NULL; static void (*fiber_entry_point)(void *, void *) = NULL;
static bool new_fiber_started = false; static bool new_fiber_started = false;
static void *arg0; static void *arg0;
static void *arg1; static void *arg1;
extern void *rb_asyncify_unwind_buf; /* Defined in wasm/setjmp.c in Ruby source code */
void *asyncify_buf; void *asyncify_buf;
bool unwound = false; bool unwound = false;
@ -82,7 +88,7 @@ MKXPZ_SANDBOX_API bool mkxp_sandbox_yield(void) {
unwound = true; unwound = true;
} }
if (rb_asyncify_unwind_buf == NULL) { if (mkxp_sandbox_complete()) {
break; break;
} }
@ -113,4 +119,4 @@ MKXPZ_SANDBOX_API bool mkxp_sandbox_yield(void) {
return false; return false;
} }
#endif /* MKXPZ_SANDBOX_EXTRA_RUBY_BINDINGS_H */ #endif /* SANDBOX_EXTRA_RUBY_BINDINGS_H */

View file

@ -34,8 +34,7 @@ MALLOC_FUNC = 'mkxp_sandbox_malloc'
# The name of the `free()` binding defined in extra-ruby-bindings.h # The name of the `free()` binding defined in extra-ruby-bindings.h
FREE_FUNC = 'mkxp_sandbox_free' FREE_FUNC = 'mkxp_sandbox_free'
# The name of the function defined in extra-ruby-bindings.h that yields to Ruby's asynchronous runtime COMPLETE_FUNC = 'mkxp_sandbox_complete'
YIELD_FUNC = 'mkxp_sandbox_yield'
################################################################################ ################################################################################
@ -56,7 +55,7 @@ ARG_HANDLERS = {
'const char *' => { 'const char *' => {
keep: true, keep: true,
buf_size: 'std::strlen(ARG) + 1', buf_size: 'std::strlen(ARG) + 1',
serialize: "std::strcpy((char *)(module_instance->w2c_memory.data + BUF), ARG);\n", serialize: "std::strcpy((char *)(bind.instance->w2c_memory.data + BUF), ARG);\n",
}, },
'const VALUE *' => { 'const VALUE *' => {
keep: true, keep: true,
@ -64,7 +63,7 @@ ARG_HANDLERS = {
buf_size: 'PREV_ARG * sizeof(VALUE)', buf_size: 'PREV_ARG * sizeof(VALUE)',
serialize: <<~HEREDOC serialize: <<~HEREDOC
for (int i = 0; i < PREV_ARG; ++i) { for (int i = 0; i < PREV_ARG; ++i) {
((VALUE *)(module_instance->w2c_memory.data + BUF))[i] = SERIALIZE_PTR(ARG[i]); ((VALUE *)(bind.instance->w2c_memory.data + BUF))[i] = SERIALIZE_PTR(ARG[i]);
} }
HEREDOC HEREDOC
}, },
@ -156,6 +155,7 @@ HEADER_START = <<~HEREDOC
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <boost/asio/coroutine.hpp>
#{MODULE_INCLUDE} #{MODULE_INCLUDE}
#include "src/sandbox/types.h" #include "src/sandbox/types.h"
@ -171,9 +171,9 @@ HEADER_START = <<~HEREDOC
struct SandboxBind { struct SandboxBind {
private: private:
wasm_ptr_t next_func_ptr; wasm_ptr_t next_func_ptr;
std::shared_ptr<struct w2c_#{MODULE_NAME}> module_instance; std::shared_ptr<struct w2c_#{MODULE_NAME}> instance;
wasm_ptr_t _sbindgen_malloc(wasm_ptr_t); wasm_ptr_t sbindgen_malloc(wasm_ptr_t);
wasm_ptr_t _sbindgen_create_func_ptr(); wasm_ptr_t sbindgen_create_func_ptr();
public: public:
SandboxBind(std::shared_ptr<struct w2c_#{MODULE_NAME}>); SandboxBind(std::shared_ptr<struct w2c_#{MODULE_NAME}>);
@ -181,7 +181,6 @@ HEADER_START = <<~HEREDOC
HEREDOC HEREDOC
HEADER_END = <<~HEREDOC HEADER_END = <<~HEREDOC
};
#endif // MKXP_SANDBOX_BINDGEN_H #endif // MKXP_SANDBOX_BINDGEN_H
HEREDOC HEREDOC
@ -211,27 +210,28 @@ PRELUDE = <<~HEREDOC
// Autogenerated by sandbox-bindgen.rb. Don't manually modify this file - modify sandbox-bindgen.rb instead! // Autogenerated by sandbox-bindgen.rb. Don't manually modify this file - modify sandbox-bindgen.rb instead!
#include <cstdarg> #include <cstdarg>
#include <boost/asio/yield.hpp>
#include "mkxp-sandbox-bindgen.h" #include "mkxp-sandbox-bindgen.h"
#if WABT_BIG_ENDIAN #if WABT_BIG_ENDIAN
#define SERIALIZE_32(value) __builtin_bswap32(value) #define SERIALIZE_32(value) __builtin_bswap32(value)
#define SERIALIZE_64(value) __builtin_bswap64(value) #define SERIALIZE_64(value) __builtin_bswap64(value)
#else #else
#define SERIALIZE_32(value) (value) #define SERIALIZE_32(value) (value)
#define SERIALIZE_64(value) (value) #define SERIALIZE_64(value) (value)
#endif // WABT_BIG_ENDIAN #endif
#define SERIALIZE_PTR(value) SERIALIZE_#{MEMORY64 ? '64' : '32'}(value) #define SERIALIZE_PTR(value) SERIALIZE_#{MEMORY64 ? '64' : '32'}(value)
SandboxBind::SandboxBind(std::shared_ptr<struct w2c_#{MODULE_NAME}> m) : next_func_ptr(m->w2c_T0.size), module_instance(m) {} SandboxBind::SandboxBind(std::shared_ptr<struct w2c_#{MODULE_NAME}> m) : next_func_ptr(-1), instance(m) {}
wasm_ptr_t SandboxBind::_sbindgen_malloc(wasm_size_t size) { wasm_ptr_t SandboxBind::sbindgen_malloc(wasm_size_t size) {
wasm_ptr_t buf = w2c_#{MODULE_NAME}_#{MALLOC_FUNC}(module_instance.get(), size); wasm_ptr_t buf = w2c_#{MODULE_NAME}_#{MALLOC_FUNC}(instance.get(), size);
// Verify that the entire allocated buffer is in valid memory // Verify that the entire allocated buffer is in valid memory
wasm_ptr_t buf_end; wasm_ptr_t buf_end;
if (buf == 0 || __builtin_add_overflow(buf, size, &buf_end) || buf_end >= module_instance->w2c_memory.size) { if (buf == 0 || __builtin_add_overflow(buf, size, &buf_end) || buf_end >= instance->w2c_memory.size) {
return 0; return 0;
} }
@ -239,30 +239,34 @@ PRELUDE = <<~HEREDOC
} }
wasm_ptr_t SandboxBind::_sbindgen_create_func_ptr() { wasm_ptr_t SandboxBind::sbindgen_create_func_ptr() {
if (next_func_ptr < module_instance->w2c_T0.max_size) { if (next_func_ptr == (wasm_ptr_t)-1) {
next_func_ptr = instance->w2c_T0.size;
}
if (next_func_ptr < instance->w2c_T0.max_size) {
return next_func_ptr++; return next_func_ptr++;
} }
// Make sure that an integer overflow won't occur if we double the max size of the funcref table // Make sure that an integer overflow won't occur if we double the max size of the funcref table
wasm_size_t new_max_size; wasm_size_t new_max_size;
if (__builtin_add_overflow(module_instance->w2c_T0.max_size, module_instance->w2c_T0.max_size, &new_max_size)) { if (__builtin_add_overflow(instance->w2c_T0.max_size, instance->w2c_T0.max_size, &new_max_size)) {
return -1; return 0;
} }
// Double the max size of the funcref table // Double the max size of the funcref table
wasm_size_t old_max_size = module_instance->w2c_T0.max_size; wasm_size_t old_max_size = instance->w2c_T0.max_size;
module_instance->w2c_T0.max_size = new_max_size; instance->w2c_T0.max_size = new_max_size;
// Double the size of the funcref table buffer // Double the size of the funcref table buffer
if (wasm_rt_grow_funcref_table(&module_instance->w2c_T0, old_max_size, wasm_rt_funcref_t { if (wasm_rt_grow_funcref_table(&instance->w2c_T0, old_max_size, wasm_rt_funcref_t {
.func_type = wasm2c_ruby_get_func_type(0, 0), .func_type = wasm2c_ruby_get_func_type(0, 0),
.func = NULL, .func = NULL,
.func_tailcallee = {.fn = NULL}, .func_tailcallee = {.fn = NULL},
.module_instance = module_instance.get(), .module_instance = instance.get(),
}) != old_max_size) { }) != old_max_size) {
module_instance->w2c_T0.max_size = old_max_size; instance->w2c_T0.max_size = old_max_size;
return -1; return 0;
} }
return next_func_ptr++; return next_func_ptr++;
@ -275,7 +279,8 @@ HEREDOC
################################################################################ ################################################################################
declarations = [] declarations = []
bindings = [] coroutines = []
func_names = []
File.readlines('tags', chomp: true).each do |line| File.readlines('tags', chomp: true).each do |line|
line = line.split("\t") line = line.split("\t")
@ -300,7 +305,8 @@ File.readlines('tags', chomp: true).each do |line|
args = args.gsub('VALUE,VALUE', '$').split(',').map { |arg| arg.gsub('$', 'VALUE,VALUE') }.map { |arg| arg == '...' ? '...' : arg.match?(/\(\* \w+\)/) ? arg.gsub(/\(\* \w+\)/, '(*)') : arg.rpartition(' ')[0].strip } args = args.gsub('VALUE,VALUE', '$').split(',').map { |arg| arg.gsub('$', 'VALUE,VALUE') }.map { |arg| arg == '...' ? '...' : arg.match?(/\(\* \w+\)/) ? arg.gsub(/\(\* \w+\)/, '(*)') : arg.rpartition(' ')[0].strip }
next unless (0...args.length).all? { |i| args[i] == '...' || (ARG_HANDLERS.include?(args[i]) && (ARG_HANDLERS[args[i]][:condition].nil? || ARG_HANDLERS[args[i]][:condition].call(func_name, args, i))) } next unless (0...args.length).all? { |i| args[i] == '...' || (ARG_HANDLERS.include?(args[i]) && (ARG_HANDLERS[args[i]][:condition].nil? || ARG_HANDLERS[args[i]][:condition].call(func_name, args, i))) }
binding = '' coroutine_initializer = ''
destructor = []
transformed_args = Set[] transformed_args = Set[]
buffers = [] buffers = []
i = 0 i = 0
@ -310,129 +316,176 @@ File.readlines('tags', chomp: true).each do |line|
# Generate bindings for converting the arguments # Generate bindings for converting the arguments
if !handler[:func_ptr_args].nil? || handler[:anyargs] if !handler[:func_ptr_args].nil? || handler[:anyargs]
binding += "wasm_ptr_t v#{i} = _sbindgen_create_func_ptr();\n" coroutine_initializer += <<~HEREDOC
binding += "if (v#{i} == (wasm_ptr_t)-1) {\n" f#{i} = bind.sbindgen_create_func_ptr();
buffers.reverse_each { |buf| binding += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(module_instance.get(), #{buf});\n" } if (f#{i} == 0) {
binding += " throw SandboxOutOfMemoryException();\n"
binding += "}\n"
if handler[:anyargs]
binding += <<~HEREDOC
module_instance->w2c_T0.data[v#{i}] = wasm_rt_funcref_t {
.func_type = wasm2c_#{MODULE_NAME}_get_func_type(a#{args.length - 1} == -1 ? 3 : a#{args.length - 1} == -2 ? 2 : a#{args.length - 1} + 1, 1, #{([:size] * 16).map { |type| FUNC_TYPE_TABLE[type] }.join(', ')}),
.func = (wasm_rt_function_ptr_t)a#{i},
.func_tailcallee = {.fn = NULL},
.module_instance = module_instance.get(),
};
HEREDOC
else
binding += <<~HEREDOC
module_instance->w2c_T0.data[v#{i}] = wasm_rt_funcref_t {
.func_type = wasm2c_#{MODULE_NAME}_get_func_type(#{handler[:func_ptr_args].length}, #{handler[:func_ptr_rets].length}#{handler[:func_ptr_args].empty? && handler[:func_ptr_rets].empty? ? '' : ', ' + (handler[:func_ptr_args] + handler[:func_ptr_rets]).map { |type| FUNC_TYPE_TABLE[type] }.join(', ')}),
.func = (wasm_rt_function_ptr_t)a#{i},
.func_tailcallee = {.fn = NULL},
.module_instance = module_instance.get(),
};
HEREDOC
end
binding += "\n"
transformed_args.add(i)
elsif !handler[:buf_size].nil?
binding += <<~HEREDOC
wasm_ptr_t v#{i} = _sbindgen_malloc(#{handler[:buf_size].gsub('PREV_ARG', "a#{i - 1}").gsub('ARG', "a#{i}")});
if (v#{i} == 0) {
HEREDOC HEREDOC
buffers.reverse_each { |buf| binding += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(module_instance.get(), #{buf});\n" } buffers.reverse_each { |buf| coroutine_initializer += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(bind.instance.get(), #{buf});\n" }
binding += <<~HEREDOC coroutine_initializer += <<~HEREDOC
throw SandboxOutOfMemoryException(); throw SandboxOutOfMemoryException();
} }
HEREDOC HEREDOC
binding += handler[:serialize].gsub('PREV_ARG', "a#{i - 1}").gsub('ARG', "a#{i}").gsub('BUF', "v#{i}") if handler[:anyargs]
binding += "\n" coroutine_initializer += <<~HEREDOC
bind.instance->w2c_T0.data[f#{i}] = wasm_rt_funcref_t {
.func_type = wasm2c_#{MODULE_NAME}_get_func_type(a#{args.length - 1} == -1 ? 3 : a#{args.length - 1} == -2 ? 2 : a#{args.length - 1} + 1, 1, #{([:size] * 16).map { |type| FUNC_TYPE_TABLE[type] }.join(', ')}),
.func = (wasm_rt_function_ptr_t)a#{i},
.func_tailcallee = {.fn = NULL},
.module_instance = bind.instance.get(),
};
HEREDOC
else
coroutine_initializer += <<~HEREDOC
bind.instance->w2c_T0.data[f#{i}] = wasm_rt_funcref_t {
.func_type = wasm2c_#{MODULE_NAME}_get_func_type(#{handler[:func_ptr_args].length}, #{handler[:func_ptr_rets].length}#{handler[:func_ptr_args].empty? && handler[:func_ptr_rets].empty? ? '' : ', ' + (handler[:func_ptr_args] + handler[:func_ptr_rets]).map { |type| FUNC_TYPE_TABLE[type] }.join(', ')}),
.func = (wasm_rt_function_ptr_t)a#{i},
.func_tailcallee = {.fn = NULL},
.module_instance = bind.instance.get(),
};
HEREDOC
end
coroutine_initializer += "\n"
transformed_args.add(i) transformed_args.add(i)
buffers.append("v#{i}") elsif !handler[:buf_size].nil?
coroutine_initializer += <<~HEREDOC
f#{i} = bind.sbindgen_malloc(#{handler[:buf_size].gsub('PREV_ARG', "a#{i - 1}").gsub('ARG', "a#{i}")});
if (f#{i} == 0) {
HEREDOC
buffers.reverse_each { |buf| coroutine_initializer += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(bind.instance.get(), #{buf});\n" }
coroutine_initializer += <<~HEREDOC
throw SandboxOutOfMemoryException();
}
HEREDOC
coroutine_initializer += handler[:serialize].gsub('PREV_ARG', "a#{i - 1}").gsub('ARG', "a#{i}").gsub('BUF', "f#{i}")
coroutine_initializer += "\n"
transformed_args.add(i)
buffers.append("f#{i}")
end end
i += 1 i += 1
end end
coroutine_vars = []
# If this is a varargs function, manually generate bindings for getting the varargs based on the function name # If this is a varargs function, manually generate bindings for getting the varargs based on the function name
if !args.empty? && args[-1] == '...' if !args.empty? && args[-1] == '...'
case func_name case func_name
when 'rb_funcall' when 'rb_funcall'
binding += <<~HEREDOC coroutine_initializer += <<~HEREDOC
wasm_ptr_t v = _sbindgen_malloc(a#{args.length - 2} * sizeof(VALUE)); f#{args.length - 1} = bind.sbindgen_malloc(a#{args.length - 2} * sizeof(VALUE));
if (v == 0) { if (f#{args.length - 1} == 0) {
HEREDOC HEREDOC
buffers.reverse_each { |buf| binding += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(module_instance.get(), #{buf});\n" } buffers.reverse_each { |buf| coroutine_initializer += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(bind.instance.get(), #{buf});\n" }
binding += <<~HEREDOC coroutine_initializer += <<~HEREDOC
throw SandboxOutOfMemoryException(); throw SandboxOutOfMemoryException();
} }
std::va_list a; std::va_list a;
va_start(a, a#{args.length - 2}); va_start(a, a#{args.length - 2});
for (long i = 0; i < a#{args.length - 2}; ++i) { for (long i = 0; i < a#{args.length - 2}; ++i) {
((VALUE *)(module_instance->w2c_memory.data + v))[i] = SERIALIZE_PTR(va_arg(a, VALUE)); ((VALUE *)(bind.instance->w2c_memory.data + f#{args.length - 1}))[i] = SERIALIZE_PTR(va_arg(a, VALUE));
} }
va_end(a); va_end(a);
HEREDOC HEREDOC
binding += "\n" coroutine_initializer += "\n"
buffers.append('v') buffers.append("f#{args.length - 1}")
when 'rb_rescue2' when 'rb_rescue2'
binding += <<~HEREDOC coroutine_vars.append('wasm_size_t n')
coroutine_initializer += <<~HEREDOC
std::va_list a, b; std::va_list a, b;
va_start(a, a#{args.length - 2}); va_start(a, a#{args.length - 2});
va_copy(b, a); va_copy(b, a);
wasm_size_t n = 0; n = 0;
do ++n; while (va_arg(b, VALUE)); do ++n; while (va_arg(b, VALUE));
va_end(b); va_end(b);
wasm_ptr_t v = _sbindgen_malloc(n * sizeof(VALUE)); f#{args.length - 1} = bind.sbindgen_malloc(n * sizeof(VALUE));
if (v == 0) { if (f#{args.length - 1} == 0) {
va_end(a); va_end(a);
HEREDOC HEREDOC
buffers.reverse_each { |buf| binding += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(module_instance.get(), #{buf});\n" } buffers.reverse_each { |buf| coroutine_initializer += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(bind.instance.get(), #{buf});\n" }
binding += <<~HEREDOC coroutine_initializer += <<~HEREDOC
throw SandboxOutOfMemoryException(); throw SandboxOutOfMemoryException();
} }
for (wasm_size_t i = 0; i < n; ++i) { for (wasm_size_t i = 0; i < n; ++i) {
((VALUE *)(module_instance->w2c_memory.data + v))[i] = SERIALIZE_PTR(va_arg(a, VALUE)); ((VALUE *)(bind.instance->w2c_memory.data + f#{args.length - 1}))[i] = SERIALIZE_PTR(va_arg(a, VALUE));
} }
HEREDOC HEREDOC
binding += "\n" coroutine_initializer += "\n"
buffers.append('v') buffers.append("f#{args.length - 1}")
else else
next next
end end
end end
# Generate bindings for running the actual function
handler = RET_HANDLERS[ret] handler = RET_HANDLERS[ret]
if handler[:primitive] != :void
binding += <<~HEREDOC fields = (0...args.length).filter_map do |i|
#{!RET_HANDLERS[ret][:keep] ? VAR_TYPE_TABLE[RET_HANDLERS[ret][:primitive]] : ret} r; (args[i] == '...' || transformed_args.include?(i)) && "wasm_ptr_t f#{i}"
do r = w2c_#{MODULE_NAME}_#{func_name}(#{(['module_instance.get()'] + (0...args.length).map { |i| args[i] == '...' ? 'v' : transformed_args.include?(i) ? "v#{i}" : "a#{i}" }).join(', ')}); while (w2c_#{MODULE_NAME}_#{YIELD_FUNC}(module_instance.get()));
HEREDOC
else
binding += "do w2c_#{MODULE_NAME}_#{func_name}(#{(['module_instance.get()'] + (0...args.length).map { |i| args[i] == '...' ? 'v' : transformed_args.include?(i) ? "v#{i}" : "a#{i}" }).join(', ')}); while (w2c_#{MODULE_NAME}_#{YIELD_FUNC}(module_instance.get()));\n"
end
buffers.reverse_each { |buf| binding += "w2c_#{MODULE_NAME}_#{FREE_FUNC}(module_instance.get(), #{buf});\n" }
if handler[:primitive] != :void
binding += "return r;\n"
end end
declarations.append("#{!RET_HANDLERS[ret][:keep] ? VAR_TYPE_TABLE[RET_HANDLERS[ret][:primitive]] : ret} #{func_name}(#{(0...args.length).map { |i| args[i] == '...' ? '...' : !ARG_HANDLERS[args[i]][:declaration].nil? ? ARG_HANDLERS[args[i]][:declaration] : !ARG_HANDLERS[args[i]][:keep] ? VAR_TYPE_TABLE[ARG_HANDLERS[args[i]][:primitive]] : args[i] }.join(', ')});\n") coroutine_ret = !RET_HANDLERS[ret][:keep] ? VAR_TYPE_TABLE[RET_HANDLERS[ret][:primitive]] : ret;
bindings.append("#{!RET_HANDLERS[ret][:keep] ? VAR_TYPE_TABLE[RET_HANDLERS[ret][:primitive]] : ret} SandboxBind::#{func_name}(#{(0...args.length).map { |i| args[i] == '...' ? '...' : !ARG_HANDLERS[args[i]][:formatter].nil? ? ARG_HANDLERS[args[i]][:formatter].call("a#{i}") : !ARG_HANDLERS[args[i]][:keep] ? "#{VAR_TYPE_TABLE[ARG_HANDLERS[args[i]][:primitive]]} a#{i}" : "#{args[i]} a#{i}" }.join(', ')}) {\n#{binding.split("\n").map { |line| " #{line}".rstrip() }.join("\n")}\n}\n")
coroutine_vars.append("#{coroutine_ret} r") if handler[:primitive] != :void
coroutine_args = ['SandboxBind &bind']
coroutine_args.append((0...args.length).map do |i|
args[i] == '...' ? '...'
: !ARG_HANDLERS[args[i]][:formatter].nil? ? ARG_HANDLERS[args[i]][:formatter].call("a#{i}")
: !ARG_HANDLERS[args[i]][:keep] ? "#{VAR_TYPE_TABLE[ARG_HANDLERS[args[i]][:primitive]]} a#{i}"
: "#{args[i]} a#{i}"
end)
declaration_args = ['SandboxBind &']
declaration_args.append((0...args.length).map do |i|
args[i] == '...' ? '...'
: !ARG_HANDLERS[args[i]][:formatter].nil? ? ARG_HANDLERS[args[i]][:formatter].call('')
: !ARG_HANDLERS[args[i]][:keep] ? "#{VAR_TYPE_TABLE[ARG_HANDLERS[args[i]][:primitive]]}"
: "#{args[i]}"
end)
coroutine_inner = <<~HEREDOC
#{handler[:primitive] == :void ? '' : 'r = '}w2c_#{MODULE_NAME}_#{func_name}(#{(['bind.instance.get()'] + (0...args.length).map { |i| args[i] == '...' || transformed_args.include?(i) ? "f#{i}" : "a#{i}" }).join(', ')});
if (w2c_#{MODULE_NAME}_#{COMPLETE_FUNC}(bind.instance.get())) break;
yield;
HEREDOC
coroutine_finalizer = (0...buffers.length).map { |i| "w2c_#{MODULE_NAME}_#{FREE_FUNC}(bind.instance.get(), #{buffers[buffers.length - 1 - i]});" }
coroutine_definition = <<~HEREDOC
#{coroutine_ret} #{func_name}::operator()(#{coroutine_args.join(', ')}) {#{coroutine_vars.empty? ? '' : (coroutine_vars.map { |var| "\n #{var} = 0;" }.join + "\n")}
reenter (this) {
#{coroutine_initializer.empty? ? '' : (coroutine_initializer.split("\n").map { |line| " #{line}" }.join("\n") + "\n\n")} for (;;) {
#{coroutine_inner.split("\n").map { |line| " #{line}" }.join("\n")}
}#{coroutine_finalizer.empty? ? '' : ("\n\n" + coroutine_finalizer.map { |line| " #{line}" }.join("\n"))}
}#{handler[:primitive] == :void ? '' : "\n\n return r;"}
}
HEREDOC
coroutine_declaration = <<~HEREDOC
struct #{func_name} : boost::asio::coroutine {
#{coroutine_ret} operator()(#{declaration_args.join(', ')});
#{fields.empty? ? '' : (" private:\n" + fields.map { |field| " #{field};\n" }.join)}};
HEREDOC
func_names.append(func_name)
coroutines.append(coroutine_definition)
declarations.append(coroutine_declaration)
end end
File.open('mkxp-sandbox-bindgen.h', 'w') do |file| File.open('mkxp-sandbox-bindgen.h', 'w') do |file|
file.write(HEADER_START) file.write(HEADER_START)
for func_name in func_names
file.write(" friend struct #{func_name};\n")
end
file.write("};\n")
for declaration in declarations for declaration in declarations
file.write(' ' + declaration) file.write("\n" + declaration)
end end
file.write(HEADER_END) file.write(HEADER_END)
end end
File.open('mkxp-sandbox-bindgen.cpp', 'w') do |file| File.open('mkxp-sandbox-bindgen.cpp', 'w') do |file|
file.write(PRELUDE) file.write(PRELUDE)
for binding in bindings for coroutine in coroutines
file.write("\n\n") file.write("\n\n")
file.write(binding) file.write(coroutine)
end end
end end

View file

@ -24,9 +24,13 @@
#include <cstdarg> #include <cstdarg>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/yield.hpp>
#include "core.h" #include "core.h"
#include "sandbox/sandbox.h" #include "sandbox/sandbox.h"
#define AWAIT(coroutine, ...) do { coroutine(__VA_ARGS__); if (coroutine.is_complete()) break; yield; } while (1)
using namespace mkxp_retro; using namespace mkxp_retro;
static void fallback_log(enum retro_log_level level, const char *fmt, ...) { static void fallback_log(enum retro_log_level level, const char *fmt, ...) {
@ -46,19 +50,37 @@ static VALUE my_cpp_func(w2c_ruby *ruby, int32_t argc, wasm_ptr_t argv, VALUE se
} }
static bool init_sandbox() { static bool init_sandbox() {
struct runtime : boost::asio::coroutine {
struct rb_eval_string eval;
struct rb_define_global_function define;
void operator()() {
reenter (this) {
AWAIT(eval, sandbox->bind, "puts 'Hello, World!'");
eval = rb_eval_string();
AWAIT(eval, sandbox->bind, "require 'zlib'; p Zlib::Deflate::deflate('hello')");
AWAIT(define, sandbox->bind, "my_cpp_func", (VALUE (*)(void *, ANYARGS))my_cpp_func, -1);
eval = rb_eval_string();
AWAIT(eval, sandbox->bind, "my_cpp_func(1, nil, 3, 'this is a string', :symbol, 2)");
eval = rb_eval_string();
AWAIT(eval, sandbox->bind, "p Dir.glob '/mkxp-retro-game/*'");
}
}
};
sandbox.reset(); sandbox.reset();
try { try {
sandbox.reset(new Sandbox(game_path)); sandbox.reset(new Sandbox(game_path));
sandbox->bind.rb_eval_string("puts 'Hello, World!'"); struct runtime runtime;
sandbox->bind.rb_eval_string("require 'zlib'; p Zlib::Deflate::deflate('hello')"); // TODO: Replace this loop with a stackful executor, otherwise you won't be able to call into the Ruby API from inside of a C/C++ function that is itself called from inside of Ruby.
do runtime(); while (w2c_ruby_mkxp_sandbox_yield(&sandbox->module_instance()));
sandbox->bind.rb_define_global_function("my_cpp_func", (VALUE (*)(void *, ANYARGS))my_cpp_func, -1);
sandbox->bind.rb_eval_string("my_cpp_func(1, nil, 3, 'this is a string', :symbol, 2)");
sandbox->bind.rb_eval_string("p Dir.glob '/mkxp-retro-game/*'");
} catch (SandboxException) { } catch (SandboxException) {
log_printf(RETRO_LOG_ERROR, "Failed to initialize Ruby\n"); log_printf(RETRO_LOG_ERROR, "Failed to initialize Ruby\n");
sandbox.reset(); sandbox.reset();

View file

@ -145,3 +145,7 @@ Sandbox::~Sandbox() {
wasm2c_ruby_free(RB); wasm2c_ruby_free(RB);
wasm_rt_free(); wasm_rt_free();
} }
w2c_ruby &Sandbox::module_instance() {
return *ruby;
}

View file

@ -38,6 +38,7 @@ struct Sandbox {
SandboxBind bind; SandboxBind bind;
Sandbox(const char *game_path); Sandbox(const char *game_path);
~Sandbox(); ~Sandbox();
struct w2c_ruby &module_instance();
}; };
#endif // MKXPZ_SANDBOX_H #endif // MKXPZ_SANDBOX_H

View file

@ -0,0 +1,4 @@
[wrap-git]
url = https://github.com/boostorg/asio
revision = boost-1.87.0
depth = 1