From c4d5d8af97c94d73f0b021c24b620af0b864a4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Tue, 7 Jan 2025 00:02:09 -0500 Subject: [PATCH] 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. --- meson.build | 7 + retro/Makefile | 4 +- retro/extra-ruby-bindings.h | 30 +++-- retro/sandbox-bindgen.rb | 249 ++++++++++++++++++++++-------------- src/core.cpp | 36 +++++- src/sandbox/sandbox.cpp | 4 + src/sandbox/sandbox.h | 1 + subprojects/boost_asio.wrap | 4 + 8 files changed, 216 insertions(+), 119 deletions(-) create mode 100644 subprojects/boost_asio.wrap diff --git a/meson.build b/meson.build index 6f39ae25..d8fa736d 100644 --- a/meson.build +++ b/meson.build @@ -35,6 +35,12 @@ if get_option('retro') == true 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.add_cmake_defines({ 'CMAKE_POSITION_INDEPENDENT_CODE': true, @@ -106,6 +112,7 @@ if get_option('retro') == true library( 'retro-' + meson.project_name(), dependencies: [ + cmake.subproject('boost_asio', options: boost_options).dependency('boost_asio'), cmake.subproject('zlib', options: zlib_options).dependency('zlibstatic'), cmake.subproject('bzip2', options: bzip2_options).dependency('bz2_static'), cmake.subproject('liblzma', options: lzma_options).dependency('liblzma'), diff --git a/retro/Makefile b/retro/Makefile index 0fae6398..dd794f38 100644 --- a/retro/Makefile +++ b/retro/Makefile @@ -86,7 +86,7 @@ $(OUTDIR)/mkxp-retro-ruby/mkxp-retro-ruby.h $(OUTDIR)/mkxp-retro-ruby/mkxp-retro mkdir -p $(OUTDIR)/mkxp-retro-ruby $(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) cd $(DOWNLOADS)/ruby && $(MAKE) install DESTDIR=$(OUTDIR) 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 ' | $(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 -$(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 \ --prefix=/mkxp-retro-dist \ --host $(TARGET) \ diff --git a/retro/extra-ruby-bindings.h b/retro/extra-ruby-bindings.h index 8fd59903..e221dfe4 100644 --- a/retro/extra-ruby-bindings.h +++ b/retro/extra-ruby-bindings.h @@ -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. */ -#ifndef MKXPZ_SANDBOX_EXTRA_RUBY_BINDINGS_H -#define MKXPZ_SANDBOX_EXTRA_RUBY_BINDINGS_H +#ifndef SANDBOX_EXTRA_RUBY_BINDINGS_H +#define SANDBOX_EXTRA_RUBY_BINDINGS_H #include #include @@ -31,43 +31,49 @@ #include "wasm/machine.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. */ -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 */ __wasm_call_ctors(); } /* 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 */ __wasm_call_dtors(); } /* 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); } /* Exposes the `free()` function. */ -MKXPZ_SANDBOX_API void mkxp_sandbox_free(void *ptr) { +MKXP_SANDBOX_API void mkxp_sandbox_free(void *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. * 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. * 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. */ -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 bool new_fiber_started = false; static void *arg0; static void *arg1; - extern void *rb_asyncify_unwind_buf; /* Defined in wasm/setjmp.c in Ruby source code */ - void *asyncify_buf; bool unwound = false; @@ -82,7 +88,7 @@ MKXPZ_SANDBOX_API bool mkxp_sandbox_yield(void) { unwound = true; } - if (rb_asyncify_unwind_buf == NULL) { + if (mkxp_sandbox_complete()) { break; } @@ -113,4 +119,4 @@ MKXPZ_SANDBOX_API bool mkxp_sandbox_yield(void) { return false; } -#endif /* MKXPZ_SANDBOX_EXTRA_RUBY_BINDINGS_H */ +#endif /* SANDBOX_EXTRA_RUBY_BINDINGS_H */ diff --git a/retro/sandbox-bindgen.rb b/retro/sandbox-bindgen.rb index c5a3656f..f5343de1 100644 --- a/retro/sandbox-bindgen.rb +++ b/retro/sandbox-bindgen.rb @@ -34,8 +34,7 @@ MALLOC_FUNC = 'mkxp_sandbox_malloc' # The name of the `free()` binding defined in extra-ruby-bindings.h FREE_FUNC = 'mkxp_sandbox_free' -# The name of the function defined in extra-ruby-bindings.h that yields to Ruby's asynchronous runtime -YIELD_FUNC = 'mkxp_sandbox_yield' +COMPLETE_FUNC = 'mkxp_sandbox_complete' ################################################################################ @@ -56,7 +55,7 @@ ARG_HANDLERS = { 'const char *' => { keep: true, 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 *' => { keep: true, @@ -64,7 +63,7 @@ ARG_HANDLERS = { buf_size: 'PREV_ARG * sizeof(VALUE)', serialize: <<~HEREDOC 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 }, @@ -156,6 +155,7 @@ HEADER_START = <<~HEREDOC #include #include #include + #include #{MODULE_INCLUDE} #include "src/sandbox/types.h" @@ -171,9 +171,9 @@ HEADER_START = <<~HEREDOC struct SandboxBind { private: wasm_ptr_t next_func_ptr; - std::shared_ptr module_instance; - wasm_ptr_t _sbindgen_malloc(wasm_ptr_t); - wasm_ptr_t _sbindgen_create_func_ptr(); + std::shared_ptr instance; + wasm_ptr_t sbindgen_malloc(wasm_ptr_t); + wasm_ptr_t sbindgen_create_func_ptr(); public: SandboxBind(std::shared_ptr); @@ -181,7 +181,6 @@ HEADER_START = <<~HEREDOC HEREDOC HEADER_END = <<~HEREDOC - }; #endif // MKXP_SANDBOX_BINDGEN_H HEREDOC @@ -211,27 +210,28 @@ PRELUDE = <<~HEREDOC // Autogenerated by sandbox-bindgen.rb. Don't manually modify this file - modify sandbox-bindgen.rb instead! #include + #include #include "mkxp-sandbox-bindgen.h" #if WABT_BIG_ENDIAN - #define SERIALIZE_32(value) __builtin_bswap32(value) - #define SERIALIZE_64(value) __builtin_bswap64(value) + #define SERIALIZE_32(value) __builtin_bswap32(value) + #define SERIALIZE_64(value) __builtin_bswap64(value) #else - #define SERIALIZE_32(value) (value) - #define SERIALIZE_64(value) (value) - #endif // WABT_BIG_ENDIAN + #define SERIALIZE_32(value) (value) + #define SERIALIZE_64(value) (value) + #endif #define SERIALIZE_PTR(value) SERIALIZE_#{MEMORY64 ? '64' : '32'}(value) - SandboxBind::SandboxBind(std::shared_ptr m) : next_func_ptr(m->w2c_T0.size), module_instance(m) {} + SandboxBind::SandboxBind(std::shared_ptr m) : next_func_ptr(-1), instance(m) {} - 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 SandboxBind::sbindgen_malloc(wasm_size_t size) { + wasm_ptr_t buf = w2c_#{MODULE_NAME}_#{MALLOC_FUNC}(instance.get(), size); // Verify that the entire allocated buffer is in valid memory 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; } @@ -239,30 +239,34 @@ PRELUDE = <<~HEREDOC } - wasm_ptr_t SandboxBind::_sbindgen_create_func_ptr() { - if (next_func_ptr < module_instance->w2c_T0.max_size) { + wasm_ptr_t SandboxBind::sbindgen_create_func_ptr() { + 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++; } // 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; - if (__builtin_add_overflow(module_instance->w2c_T0.max_size, module_instance->w2c_T0.max_size, &new_max_size)) { - return -1; + if (__builtin_add_overflow(instance->w2c_T0.max_size, instance->w2c_T0.max_size, &new_max_size)) { + return 0; } // Double the max size of the funcref table - wasm_size_t old_max_size = module_instance->w2c_T0.max_size; - module_instance->w2c_T0.max_size = new_max_size; + wasm_size_t old_max_size = instance->w2c_T0.max_size; + instance->w2c_T0.max_size = new_max_size; // 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 = NULL, .func_tailcallee = {.fn = NULL}, - .module_instance = module_instance.get(), + .module_instance = instance.get(), }) != old_max_size) { - module_instance->w2c_T0.max_size = old_max_size; - return -1; + instance->w2c_T0.max_size = old_max_size; + return 0; } return next_func_ptr++; @@ -275,7 +279,8 @@ HEREDOC ################################################################################ declarations = [] -bindings = [] +coroutines = [] +func_names = [] File.readlines('tags', chomp: true).each do |line| 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 } 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[] buffers = [] i = 0 @@ -310,129 +316,176 @@ File.readlines('tags', chomp: true).each do |line| # Generate bindings for converting the arguments if !handler[:func_ptr_args].nil? || handler[:anyargs] - binding += "wasm_ptr_t v#{i} = _sbindgen_create_func_ptr();\n" - binding += "if (v#{i} == (wasm_ptr_t)-1) {\n" - buffers.reverse_each { |buf| binding += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(module_instance.get(), #{buf});\n" } - 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) { + coroutine_initializer += <<~HEREDOC + f#{i} = bind.sbindgen_create_func_ptr(); + if (f#{i} == 0) { HEREDOC - buffers.reverse_each { |buf| binding += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(module_instance.get(), #{buf});\n" } - binding += <<~HEREDOC + buffers.reverse_each { |buf| coroutine_initializer += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(bind.instance.get(), #{buf});\n" } + coroutine_initializer += <<~HEREDOC throw SandboxOutOfMemoryException(); } HEREDOC - binding += handler[:serialize].gsub('PREV_ARG', "a#{i - 1}").gsub('ARG', "a#{i}").gsub('BUF', "v#{i}") - binding += "\n" + if handler[:anyargs] + 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) - 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 i += 1 end + coroutine_vars = [] + # If this is a varargs function, manually generate bindings for getting the varargs based on the function name if !args.empty? && args[-1] == '...' case func_name when 'rb_funcall' - binding += <<~HEREDOC - wasm_ptr_t v = _sbindgen_malloc(a#{args.length - 2} * sizeof(VALUE)); - if (v == 0) { + coroutine_initializer += <<~HEREDOC + f#{args.length - 1} = bind.sbindgen_malloc(a#{args.length - 2} * sizeof(VALUE)); + if (f#{args.length - 1} == 0) { HEREDOC - buffers.reverse_each { |buf| binding += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(module_instance.get(), #{buf});\n" } - binding += <<~HEREDOC + buffers.reverse_each { |buf| coroutine_initializer += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(bind.instance.get(), #{buf});\n" } + coroutine_initializer += <<~HEREDOC throw SandboxOutOfMemoryException(); } std::va_list a; va_start(a, a#{args.length - 2}); 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); HEREDOC - binding += "\n" - buffers.append('v') + coroutine_initializer += "\n" + buffers.append("f#{args.length - 1}") when 'rb_rescue2' - binding += <<~HEREDOC + coroutine_vars.append('wasm_size_t n') + coroutine_initializer += <<~HEREDOC std::va_list a, b; va_start(a, a#{args.length - 2}); va_copy(b, a); - wasm_size_t n = 0; + n = 0; do ++n; while (va_arg(b, VALUE)); va_end(b); - wasm_ptr_t v = _sbindgen_malloc(n * sizeof(VALUE)); - if (v == 0) { + f#{args.length - 1} = bind.sbindgen_malloc(n * sizeof(VALUE)); + if (f#{args.length - 1} == 0) { va_end(a); HEREDOC - buffers.reverse_each { |buf| binding += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(module_instance.get(), #{buf});\n" } - binding += <<~HEREDOC + buffers.reverse_each { |buf| coroutine_initializer += " w2c_#{MODULE_NAME}_#{FREE_FUNC}(bind.instance.get(), #{buf});\n" } + coroutine_initializer += <<~HEREDOC throw SandboxOutOfMemoryException(); } 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 - binding += "\n" - buffers.append('v') + coroutine_initializer += "\n" + buffers.append("f#{args.length - 1}") else next end end - # Generate bindings for running the actual function handler = RET_HANDLERS[ret] - if handler[:primitive] != :void - binding += <<~HEREDOC - #{!RET_HANDLERS[ret][:keep] ? VAR_TYPE_TABLE[RET_HANDLERS[ret][:primitive]] : ret} r; - 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" + + fields = (0...args.length).filter_map do |i| + (args[i] == '...' || transformed_args.include?(i)) && "wasm_ptr_t f#{i}" 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") - 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_ret = !RET_HANDLERS[ret][:keep] ? VAR_TYPE_TABLE[RET_HANDLERS[ret][:primitive]] : ret; + + 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 File.open('mkxp-sandbox-bindgen.h', 'w') do |file| 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 - file.write(' ' + declaration) + file.write("\n" + declaration) end file.write(HEADER_END) end File.open('mkxp-sandbox-bindgen.cpp', 'w') do |file| file.write(PRELUDE) - for binding in bindings + for coroutine in coroutines file.write("\n\n") - file.write(binding) + file.write(coroutine) end end diff --git a/src/core.cpp b/src/core.cpp index 06e1e2a6..7c26bbc1 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -24,9 +24,13 @@ #include #include #include +#include +#include #include "core.h" #include "sandbox/sandbox.h" +#define AWAIT(coroutine, ...) do { coroutine(__VA_ARGS__); if (coroutine.is_complete()) break; yield; } while (1) + using namespace mkxp_retro; 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() { + 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(); try { 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')"); - - 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/*'"); + // 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())); } catch (SandboxException) { log_printf(RETRO_LOG_ERROR, "Failed to initialize Ruby\n"); sandbox.reset(); diff --git a/src/sandbox/sandbox.cpp b/src/sandbox/sandbox.cpp index 8f0e1227..dbe186a4 100644 --- a/src/sandbox/sandbox.cpp +++ b/src/sandbox/sandbox.cpp @@ -145,3 +145,7 @@ Sandbox::~Sandbox() { wasm2c_ruby_free(RB); wasm_rt_free(); } + +w2c_ruby &Sandbox::module_instance() { + return *ruby; +} diff --git a/src/sandbox/sandbox.h b/src/sandbox/sandbox.h index 77e5abf2..3bdcc146 100644 --- a/src/sandbox/sandbox.h +++ b/src/sandbox/sandbox.h @@ -38,6 +38,7 @@ struct Sandbox { SandboxBind bind; Sandbox(const char *game_path); ~Sandbox(); + struct w2c_ruby &module_instance(); }; #endif // MKXPZ_SANDBOX_H diff --git a/subprojects/boost_asio.wrap b/subprojects/boost_asio.wrap new file mode 100644 index 00000000..b11a262a --- /dev/null +++ b/subprojects/boost_asio.wrap @@ -0,0 +1,4 @@ +[wrap-git] +url = https://github.com/boostorg/asio +revision = boost-1.87.0 +depth = 1