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