diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4395a231b..a70000db91 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: - run: ./bin/setup - run: bundle exec rake check:type - run: sudo apt-get install clang-format - - run: ./tools/clang-format-diff.sh + - run: ./bin/clang-format-diff.sh - run: bundle exec rake check:bindgen - run: git diff --exit-code diff --git a/tools/clang-format-diff.sh b/bin/clang-format-diff.sh similarity index 100% rename from tools/clang-format-diff.sh rename to bin/clang-format-diff.sh diff --git a/tools/lib/syntax_tree/minify_ruby.rb b/bin/lib/syntax_tree/minify_ruby.rb similarity index 100% rename from tools/lib/syntax_tree/minify_ruby.rb rename to bin/lib/syntax_tree/minify_ruby.rb diff --git a/tools/exe/rbminify b/bin/rbminify similarity index 80% rename from tools/exe/rbminify rename to bin/rbminify index 8de6178be6..7ecc21f057 100755 --- a/tools/exe/rbminify +++ b/bin/rbminify @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -$LOAD_PATH << File.join(File.dirname(__FILE__), "../lib") +$LOAD_PATH << File.join(File.dirname(__FILE__), "lib") require "syntax_tree" require "syntax_tree/cli" diff --git a/bin/repl.mjs b/bin/repl.mjs new file mode 100644 index 0000000000..e66a0de35e --- /dev/null +++ b/bin/repl.mjs @@ -0,0 +1,121 @@ +import * as preview2Shim from "@bytecodealliance/preview2-shim" +import * as nodeWasi from "wasi"; +import fs from "fs/promises"; +import path from "path"; +import { RubyVM } from "@ruby/wasm-wasi"; +import * as readline from 'node:readline/promises'; +import { stdin as input, stdout as output } from 'node:process'; +import { parseArgs } from "node:util" + +async function instantiateComponent(pkgPath) { + const componentJsPath = path.resolve(pkgPath, "dist/component/ruby.component.js"); + const { instantiate } = await import(componentJsPath); + const getCoreModule = async (relativePath) => { + const coreModulePath = path.resolve(pkgPath, "dist/component", relativePath); + const buffer = await fs.readFile(coreModulePath); + return WebAssembly.compile(buffer); + } + const { cli, filesystem } = preview2Shim; + cli._setArgs(["ruby.wasm"].concat(process.argv.slice(2))); + cli._setCwd("/") + filesystem._setPreopens({}) + + const { vm } = await RubyVM.instantiateComponent({ + instantiate, getCoreModule, wasip2: preview2Shim, + }); + return vm; +} + +async function instantiateModule(pkgPath) { + const binaryPath = path.resolve(pkgPath, "dist/ruby.debug+stdlib.wasm"); + const binary = await fs.readFile(binaryPath); + const rubyModule = await WebAssembly.compile(binary); + const wasi = new nodeWasi.WASI({ + stdio: "inherit", + args: ["ruby.wasm"].concat(process.argv.slice(2)), + env: process.env, + version: "preview1", + }); + + const { vm } = await RubyVM.instantiateModule({ + module: rubyModule, wasip1: wasi + }) + return vm; +}; + +function parseOptions(args) { + /** @type {import("util").ParseArgsConfig["options"]} */ + const options = { + pkg: { + type: "string", + short: "p", + default: (() => { + const dirname = path.dirname(new URL(import.meta.url).pathname); + return path.resolve(dirname, "../packages/npm-packages/ruby-head-wasm-wasi"); + })() + }, + type: { + type: "string", + short: "t", + default: "component", + }, + help: { + type: "boolean", + short: "h", + }, + } + const { values } = parseArgs({ args, options }); + return values; +} + +function printUsage() { + console.log("Usage: repl.mjs [--pkg ] [--type ]"); +} + +async function main() { + const args = parseOptions(process.argv.slice(2)); + if (args["help"]) { + printUsage(); + return; + } + const pkgPath = args["pkg"]; + const vm = await (async () => { + switch (args["type"]) { + case "component": { + console.log(`Loading component from ${pkgPath}`); + return await instantiateComponent(pkgPath); + } + case "module": { + console.log(`Loading core module from ${pkgPath}`); + return await instantiateModule(pkgPath); + } + default: + throw new Error(`Unknown type: ${args["type"]}`); + } + })(); + const rl = readline.createInterface({ input, output }); + + vm.eval(`puts RUBY_DESCRIPTION`); + + const printer = vm.eval(` + class ReplPrinter + def puts(*args) + args.each do |arg| + Kernel.puts(arg) + end + end + end + ReplPrinter.new + `); + while (true) { + const line = await rl.question(`>> `); + try { + const result = vm.eval(line); + printer.call("puts", result); + } catch (e) { + console.error(e); + } + } +} + +main().catch(console.error); diff --git a/packages/gems/js/ext/js/bindgen/ext.c b/packages/gems/js/ext/js/bindgen/ext.c index a1aa6e5b9f..87f28dc441 100644 --- a/packages/gems/js/ext/js/bindgen/ext.c +++ b/packages/gems/js/ext/js/bindgen/ext.c @@ -78,9 +78,6 @@ extern void __wasm_import_ruby_js_js_runtime_throw_prohibit_rewind_exception(uin - - - __attribute__((__weak__, __export_name__("cabi_post_ruby:js/ruby-runtime#rstring-ptr"))) void __wasm_export_exports_ruby_js_ruby_runtime_rstring_ptr_post_return(uint8_t * arg0) { if ((*((size_t*) (arg0 + 4))) > 0) { @@ -150,32 +147,6 @@ void ruby_js_js_runtime_list_borrow_js_abi_value_free(ruby_js_js_runtime_list_bo } } -__attribute__((__import_module__("[export]ruby:js/ruby-runtime"), __import_name__("[resource-drop]rb-iseq"))) -extern void __wasm_import_exports_ruby_js_ruby_runtime_rb_iseq_drop(int32_t handle); - -void exports_ruby_js_ruby_runtime_rb_iseq_drop_own(exports_ruby_js_ruby_runtime_own_rb_iseq_t handle) { - __wasm_import_exports_ruby_js_ruby_runtime_rb_iseq_drop(handle.__handle); -} - -__attribute__(( __import_module__("[export]ruby:js/ruby-runtime"), __import_name__("[resource-new]rb-iseq"))) -extern int32_t __wasm_import_exports_ruby_js_ruby_runtime_rb_iseq_new(int32_t); - -__attribute__((__import_module__("[export]ruby:js/ruby-runtime"), __import_name__("[resource-rep]rb-iseq"))) -extern int32_t __wasm_import_exports_ruby_js_ruby_runtime_rb_iseq_rep(int32_t); - -exports_ruby_js_ruby_runtime_own_rb_iseq_t exports_ruby_js_ruby_runtime_rb_iseq_new(exports_ruby_js_ruby_runtime_rb_iseq_t *rep) { - return (exports_ruby_js_ruby_runtime_own_rb_iseq_t) { __wasm_import_exports_ruby_js_ruby_runtime_rb_iseq_new((int32_t) rep) }; -} - -exports_ruby_js_ruby_runtime_rb_iseq_t* exports_ruby_js_ruby_runtime_rb_iseq_rep(exports_ruby_js_ruby_runtime_own_rb_iseq_t handle) { - return (exports_ruby_js_ruby_runtime_rb_iseq_t*) __wasm_import_exports_ruby_js_ruby_runtime_rb_iseq_rep(handle.__handle); -} - -__attribute__((__export_name__("ruby:js/ruby-runtime#[dtor]rb_iseq"))) -void __wasm_export_exports_ruby_js_ruby_runtime_rb_iseq_dtor(exports_ruby_js_ruby_runtime_rb_iseq_t* arg) { - exports_ruby_js_ruby_runtime_rb_iseq_destructor(arg); -} - __attribute__((__import_module__("[export]ruby:js/ruby-runtime"), __import_name__("[resource-drop]rb-abi-value"))) extern void __wasm_import_exports_ruby_js_ruby_runtime_rb_abi_value_drop(int32_t handle); @@ -437,27 +408,9 @@ void __wasm_export_exports_ruby_js_ruby_runtime_ruby_show_version(void) { } __attribute__((__export_name__("ruby:js/ruby-runtime#ruby-init"))) -void __wasm_export_exports_ruby_js_ruby_runtime_ruby_init(void) { - exports_ruby_js_ruby_runtime_ruby_init(); -} - -__attribute__((__export_name__("ruby:js/ruby-runtime#ruby-sysinit"))) -void __wasm_export_exports_ruby_js_ruby_runtime_ruby_sysinit(uint8_t * arg, size_t arg0) { +void __wasm_export_exports_ruby_js_ruby_runtime_ruby_init(uint8_t * arg, size_t arg0) { ext_list_string_t arg1 = (ext_list_string_t) { (ext_string_t*)(arg), (arg0) }; - exports_ruby_js_ruby_runtime_ruby_sysinit(&arg1); -} - -__attribute__((__export_name__("ruby:js/ruby-runtime#ruby-options"))) -int32_t __wasm_export_exports_ruby_js_ruby_runtime_ruby_options(uint8_t * arg, size_t arg0) { - ext_list_string_t arg1 = (ext_list_string_t) { (ext_string_t*)(arg), (arg0) }; - exports_ruby_js_ruby_runtime_own_rb_iseq_t ret = exports_ruby_js_ruby_runtime_ruby_options(&arg1); - return (ret).__handle; -} - -__attribute__((__export_name__("ruby:js/ruby-runtime#ruby-script"))) -void __wasm_export_exports_ruby_js_ruby_runtime_ruby_script(uint8_t * arg, size_t arg0) { - ext_string_t arg1 = (ext_string_t) { (uint8_t*)(arg), (arg0) }; - exports_ruby_js_ruby_runtime_ruby_script(&arg1); + exports_ruby_js_ruby_runtime_ruby_init(&arg1); } __attribute__((__export_name__("ruby:js/ruby-runtime#ruby-init-loadpath"))) diff --git a/packages/gems/js/ext/js/bindgen/ext.h b/packages/gems/js/ext/js/bindgen/ext.h index 07baf87291..925e943077 100644 --- a/packages/gems/js/ext/js/bindgen/ext.h +++ b/packages/gems/js/ext/js/bindgen/ext.h @@ -48,14 +48,6 @@ typedef struct ruby_js_js_runtime_list_borrow_js_abi_value_t { size_t len; } ruby_js_js_runtime_list_borrow_js_abi_value_t; -typedef struct exports_ruby_js_ruby_runtime_own_rb_iseq_t { - int32_t __handle; -} exports_ruby_js_ruby_runtime_own_rb_iseq_t; - -typedef struct exports_ruby_js_ruby_runtime_rb_iseq_t exports_ruby_js_ruby_runtime_rb_iseq_t; - -typedef exports_ruby_js_ruby_runtime_rb_iseq_t* exports_ruby_js_ruby_runtime_borrow_rb_iseq_t; - typedef struct exports_ruby_js_ruby_runtime_own_rb_abi_value_t { int32_t __handle; } exports_ruby_js_ruby_runtime_own_rb_abi_value_t; @@ -108,10 +100,7 @@ extern void ruby_js_js_runtime_throw_prohibit_rewind_exception(ext_string_t *mes // Exported Functions from `ruby:js/ruby-runtime` void exports_ruby_js_ruby_runtime_ruby_show_version(void); -void exports_ruby_js_ruby_runtime_ruby_init(void); -void exports_ruby_js_ruby_runtime_ruby_sysinit(ext_list_string_t *args); -exports_ruby_js_ruby_runtime_own_rb_iseq_t exports_ruby_js_ruby_runtime_ruby_options(ext_list_string_t *args); -void exports_ruby_js_ruby_runtime_ruby_script(ext_string_t *name); +void exports_ruby_js_ruby_runtime_ruby_init(ext_list_string_t *args); void exports_ruby_js_ruby_runtime_ruby_init_loadpath(void); void exports_ruby_js_ruby_runtime_rb_eval_string_protect(ext_string_t *str, exports_ruby_js_ruby_runtime_tuple2_own_rb_abi_value_s32_t *ret); void exports_ruby_js_ruby_runtime_rb_funcallv_protect(exports_ruby_js_ruby_runtime_borrow_rb_abi_value_t recv, exports_ruby_js_ruby_runtime_rb_id_t mid, exports_ruby_js_ruby_runtime_list_borrow_rb_abi_value_t *args, exports_ruby_js_ruby_runtime_tuple2_own_rb_abi_value_s32_t *ret); @@ -137,12 +126,6 @@ void ruby_js_js_runtime_raw_integer_free(ruby_js_js_runtime_raw_integer_t *ptr); void ruby_js_js_runtime_list_borrow_js_abi_value_free(ruby_js_js_runtime_list_borrow_js_abi_value_t *ptr); -extern void exports_ruby_js_ruby_runtime_rb_iseq_drop_own(exports_ruby_js_ruby_runtime_own_rb_iseq_t handle); - -extern exports_ruby_js_ruby_runtime_own_rb_iseq_t exports_ruby_js_ruby_runtime_rb_iseq_new(exports_ruby_js_ruby_runtime_rb_iseq_t *rep); -extern exports_ruby_js_ruby_runtime_rb_iseq_t* exports_ruby_js_ruby_runtime_rb_iseq_rep(exports_ruby_js_ruby_runtime_own_rb_iseq_t handle); -void exports_ruby_js_ruby_runtime_rb_iseq_destructor(exports_ruby_js_ruby_runtime_rb_iseq_t *rep); - extern void exports_ruby_js_ruby_runtime_rb_abi_value_drop_own(exports_ruby_js_ruby_runtime_own_rb_abi_value_t handle); extern exports_ruby_js_ruby_runtime_own_rb_abi_value_t exports_ruby_js_ruby_runtime_rb_abi_value_new(exports_ruby_js_ruby_runtime_rb_abi_value_t *rep); diff --git a/packages/gems/js/ext/js/bindgen/ext_component_type.o b/packages/gems/js/ext/js/bindgen/ext_component_type.o index 3f0223537f..c8a5e81716 100644 Binary files a/packages/gems/js/ext/js/bindgen/ext_component_type.o and b/packages/gems/js/ext/js/bindgen/ext_component_type.o differ diff --git a/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.c b/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.c index ec95070cc1..a0cbdc3c20 100644 --- a/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.c +++ b/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.c @@ -14,40 +14,6 @@ size_t new_size return ret; } -__attribute__((import_module("canonical_abi"), import_name("resource_drop_rb-iseq"))) -void __resource_rb_iseq_drop(uint32_t idx); - -void rb_abi_guest_rb_iseq_free(rb_abi_guest_rb_iseq_t *ptr) { - __resource_rb_iseq_drop(ptr->idx); -} - -__attribute__((import_module("canonical_abi"), import_name("resource_clone_rb-iseq"))) -uint32_t __resource_rb_iseq_clone(uint32_t idx); - -rb_abi_guest_rb_iseq_t rb_abi_guest_rb_iseq_clone(rb_abi_guest_rb_iseq_t *ptr) { - return (rb_abi_guest_rb_iseq_t){__resource_rb_iseq_clone(ptr->idx)}; -} - -__attribute__((import_module("canonical_abi"), import_name("resource_new_rb-iseq"))) -uint32_t __resource_rb_iseq_new(uint32_t val); - -rb_abi_guest_rb_iseq_t rb_abi_guest_rb_iseq_new(void *data) { - return (rb_abi_guest_rb_iseq_t){__resource_rb_iseq_new((uint32_t) data)}; -} - -__attribute__((import_module("canonical_abi"), import_name("resource_get_rb-iseq"))) -uint32_t __resource_rb_iseq_get(uint32_t idx); - -void* rb_abi_guest_rb_iseq_get(rb_abi_guest_rb_iseq_t *ptr) { - return (void*) __resource_rb_iseq_get(ptr->idx); -} - -__attribute__((export_name("canonical_abi_drop_rb-iseq"))) -void __resource_rb_iseq_dtor(uint32_t val) { - if (rb_abi_guest_rb_iseq_dtor) - rb_abi_guest_rb_iseq_dtor((void*) val); -} - __attribute__((import_module("canonical_abi"), import_name("resource_drop_rb-abi-value"))) void __resource_rb_abi_value_drop(uint32_t idx); @@ -127,25 +93,10 @@ __attribute__((export_name("ruby-show-version: func() -> ()"))) void __wasm_export_rb_abi_guest_ruby_show_version(void) { rb_abi_guest_ruby_show_version(); } -__attribute__((export_name("ruby-init: func() -> ()"))) -void __wasm_export_rb_abi_guest_ruby_init(void) { - rb_abi_guest_ruby_init(); -} -__attribute__((export_name("ruby-sysinit: func(args: list) -> ()"))) -void __wasm_export_rb_abi_guest_ruby_sysinit(int32_t arg, int32_t arg0) { +__attribute__((export_name("ruby-init: func(args: list) -> ()"))) +void __wasm_export_rb_abi_guest_ruby_init(int32_t arg, int32_t arg0) { rb_abi_guest_list_string_t arg1 = (rb_abi_guest_list_string_t) { (rb_abi_guest_string_t*)(arg), (size_t)(arg0) }; - rb_abi_guest_ruby_sysinit(&arg1); -} -__attribute__((export_name("ruby-options: func(args: list) -> handle"))) -int32_t __wasm_export_rb_abi_guest_ruby_options(int32_t arg, int32_t arg0) { - rb_abi_guest_list_string_t arg1 = (rb_abi_guest_list_string_t) { (rb_abi_guest_string_t*)(arg), (size_t)(arg0) }; - rb_abi_guest_rb_iseq_t ret = rb_abi_guest_ruby_options(&arg1); - return (ret).idx; -} -__attribute__((export_name("ruby-script: func(name: string) -> ()"))) -void __wasm_export_rb_abi_guest_ruby_script(int32_t arg, int32_t arg0) { - rb_abi_guest_string_t arg1 = (rb_abi_guest_string_t) { (char*)(arg), (size_t)(arg0) }; - rb_abi_guest_ruby_script(&arg1); + rb_abi_guest_ruby_init(&arg1); } __attribute__((export_name("ruby-init-loadpath: func() -> ()"))) void __wasm_export_rb_abi_guest_ruby_init_loadpath(void) { diff --git a/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.h b/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.h index 99b6149f77..c39fd722aa 100644 --- a/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.h +++ b/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.h @@ -8,17 +8,6 @@ extern "C" #include #include - typedef struct { - uint32_t idx; - } rb_abi_guest_rb_iseq_t; - void rb_abi_guest_rb_iseq_free(rb_abi_guest_rb_iseq_t *ptr); - rb_abi_guest_rb_iseq_t rb_abi_guest_rb_iseq_clone(rb_abi_guest_rb_iseq_t *ptr); - rb_abi_guest_rb_iseq_t rb_abi_guest_rb_iseq_new(void *data); - void* rb_abi_guest_rb_iseq_get(rb_abi_guest_rb_iseq_t *ptr); - - __attribute__((weak)) - void rb_abi_guest_rb_iseq_dtor(void *data); - typedef struct { uint32_t idx; } rb_abi_guest_rb_abi_value_t; @@ -56,10 +45,7 @@ extern "C" } rb_abi_guest_list_rb_abi_value_t; void rb_abi_guest_list_rb_abi_value_free(rb_abi_guest_list_rb_abi_value_t *ptr); void rb_abi_guest_ruby_show_version(void); - void rb_abi_guest_ruby_init(void); - void rb_abi_guest_ruby_sysinit(rb_abi_guest_list_string_t *args); - rb_abi_guest_rb_iseq_t rb_abi_guest_ruby_options(rb_abi_guest_list_string_t *args); - void rb_abi_guest_ruby_script(rb_abi_guest_string_t *name); + void rb_abi_guest_ruby_init(rb_abi_guest_list_string_t *args); void rb_abi_guest_ruby_init_loadpath(void); void rb_abi_guest_rb_eval_string_protect(rb_abi_guest_string_t *str, rb_abi_guest_tuple2_rb_abi_value_s32_t *ret0); void rb_abi_guest_rb_funcallv_protect(rb_abi_guest_rb_abi_value_t recv, rb_abi_guest_rb_id_t mid, rb_abi_guest_list_rb_abi_value_t *args, rb_abi_guest_tuple2_rb_abi_value_s32_t *ret0); diff --git a/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.wit b/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.wit index 8b8fa9cf70..586e22d375 100644 --- a/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.wit +++ b/packages/gems/js/ext/js/bindgen/legacy/rb-abi-guest.wit @@ -1,13 +1,9 @@ -resource rb-iseq resource rb-abi-value type rb-errno = s32 type rb-id = u32 ruby-show-version: func() -ruby-init: func() -ruby-sysinit: func(args: list) -ruby-options: func(args: list) -> rb-iseq -ruby-script: func(name: string) +ruby-init: func(args: list) ruby-init-loadpath: func() rb-eval-string-protect: func(str: string) -> tuple rb-funcallv-protect: func(recv: rb-abi-value, mid: rb-id, args: list) -> tuple diff --git a/packages/gems/js/ext/js/types.h b/packages/gems/js/ext/js/types.h index ae0b45443f..b0063aa5fd 100644 --- a/packages/gems/js/ext/js/types.h +++ b/packages/gems/js/ext/js/types.h @@ -10,7 +10,6 @@ typedef exports_ruby_js_ruby_runtime_own_rb_abi_value_t rb_abi_guest_own_rb_abi_value_t; typedef exports_ruby_js_ruby_runtime_list_borrow_rb_abi_value_t rb_abi_guest_list_rb_abi_value_t; -typedef exports_ruby_js_ruby_runtime_own_rb_iseq_t rb_abi_guest_rb_iseq_t; typedef exports_ruby_js_ruby_runtime_rb_id_t rb_abi_guest_rb_id_t; typedef exports_ruby_js_ruby_runtime_tuple2_own_rb_abi_value_s32_t rb_abi_guest_tuple2_rb_abi_value_s32_t; diff --git a/packages/gems/js/ext/js/witapi-core.c b/packages/gems/js/ext/js/witapi-core.c index 6a51132fe1..cf3f718a4d 100644 --- a/packages/gems/js/ext/js/witapi-core.c +++ b/packages/gems/js/ext/js/witapi-core.c @@ -188,9 +188,6 @@ void rb_abi_guest_rb_abi_value_dtor(void *data) { } #ifdef JS_ENABLE_COMPONENT_MODEL -void exports_ruby_js_ruby_runtime_rb_iseq_destructor( - exports_ruby_js_ruby_runtime_rb_iseq_t *rep) {} - void exports_ruby_js_ruby_runtime_rb_abi_value_destructor( exports_ruby_js_ruby_runtime_rb_abi_value_t *rep) { rb_abi_guest_rb_abi_value_dtor((void *)rep); @@ -203,36 +200,22 @@ void exports_ruby_js_ruby_runtime_rb_abi_value_destructor( void rb_abi_guest_ruby_show_version(void) { ruby_show_version(); } -__attribute__((noinline)) static void rb_abi_guest_ruby_init_thunk(void) { +__attribute__((noinline)) static void +rb_abi_guest_ruby_init_thunk(int argc, char **c_args) { ruby_init(); rb_abi_guest_arena_hash = rb_hash_new(); rb_abi_guest_refcount_hash = rb_hash_new(); rb_gc_register_mark_object(rb_abi_guest_arena_hash); rb_gc_register_mark_object(rb_abi_guest_refcount_hash); + ruby_sysinit(&argc, &c_args); + ruby_options(argc, c_args); } -void rb_abi_guest_ruby_init(void) { - RB_WASM_LIB_RT(rb_abi_guest_ruby_init_thunk()) -} - -void rb_abi_guest_ruby_sysinit(rb_abi_guest_list_string_t *args) { - char **c_args; +void rb_abi_guest_ruby_init(rb_abi_guest_list_string_t *args) { int argc = args->len; - c_strings_from_abi(args, c_args); - RB_WASM_LIB_RT(ruby_sysinit(&argc, &c_args)) -} - -rb_abi_guest_rb_iseq_t -rb_abi_guest_ruby_options(rb_abi_guest_list_string_t *args) { - void *result; char **c_args; c_strings_from_abi(args, c_args); - RB_WASM_LIB_RT(result = ruby_options(args->len, c_args)) - return rb_abi_guest_rb_iseq_new(result); -} - -void rb_abi_guest_ruby_script(rb_abi_guest_string_t *name) { - RB_WASM_LIB_RT(ruby_script((const char *)name->ptr)) + RB_WASM_LIB_RT(rb_abi_guest_ruby_init_thunk(argc, c_args)) } void rb_abi_guest_ruby_init_loadpath(void) { @@ -398,22 +381,9 @@ void exports_ruby_js_ruby_runtime_ruby_show_version(void) { __wasm_call_ctors_if_needed(); rb_abi_guest_ruby_show_version(); } -void exports_ruby_js_ruby_runtime_ruby_init(void) { - __wasm_call_ctors_if_needed(); - rb_abi_guest_ruby_init(); -} -void exports_ruby_js_ruby_runtime_ruby_sysinit(ext_list_string_t *args) { - __wasm_call_ctors_if_needed(); - rb_abi_guest_ruby_sysinit(args); -} -exports_ruby_js_ruby_runtime_own_rb_iseq_t -exports_ruby_js_ruby_runtime_ruby_options(ext_list_string_t *args) { - __wasm_call_ctors_if_needed(); - return rb_abi_guest_ruby_options(args); -} -void exports_ruby_js_ruby_runtime_ruby_script(ext_string_t *name) { +void exports_ruby_js_ruby_runtime_ruby_init(ext_list_string_t *args) { __wasm_call_ctors_if_needed(); - rb_abi_guest_ruby_script(name); + rb_abi_guest_ruby_init(args); } void exports_ruby_js_ruby_runtime_ruby_init_loadpath(void) { __wasm_call_ctors_if_needed(); diff --git a/packages/gems/js/wit/ruby-runtime.wit b/packages/gems/js/wit/ruby-runtime.wit index 2b174d7179..97d8da483d 100644 --- a/packages/gems/js/wit/ruby-runtime.wit +++ b/packages/gems/js/wit/ruby-runtime.wit @@ -3,17 +3,13 @@ package ruby:js; interface ruby-runtime { use js-runtime.{js-abi-value}; - resource rb-iseq {} resource rb-abi-value {} type rb-errno = s32; type rb-id = u32; ruby-show-version: func(); - ruby-init: func(); - ruby-sysinit: func(args: list); - ruby-options: func(args: list) -> rb-iseq; - ruby-script: func(name: string); + ruby-init: func(args: list); ruby-init-loadpath: func(); rb-eval-string-protect: func(str: string) -> tuple; rb-funcallv-protect: func(recv: borrow, mid: rb-id, args: list>) -> tuple; diff --git a/packages/npm-packages/ruby-wasm-wasi/src/bindgen/interfaces/ruby-js-js-runtime.d.ts b/packages/npm-packages/ruby-wasm-wasi/src/bindgen/interfaces/ruby-js-js-runtime.d.ts index 3a1a948fb5..88a3c388bb 100644 --- a/packages/npm-packages/ruby-wasm-wasi/src/bindgen/interfaces/ruby-js-js-runtime.d.ts +++ b/packages/npm-packages/ruby-wasm-wasi/src/bindgen/interfaces/ruby-js-js-runtime.d.ts @@ -16,7 +16,7 @@ export namespace RubyJsJsRuntime { export function jsValueTypeof(value: JsAbiValue): string; export function jsValueEqual(lhs: JsAbiValue, rhs: JsAbiValue): boolean; export function jsValueStrictlyEqual(lhs: JsAbiValue, rhs: JsAbiValue): boolean; - export function reflectApply(target: JsAbiValue, thisArgument: JsAbiValue, arguments: JsAbiValue[]): JsAbiResult; + export function reflectApply(target: JsAbiValue, thisArgument: JsAbiValue, arguments: Array): JsAbiResult; export function reflectGet(target: JsAbiValue, propertyKey: string): JsAbiResult; export function reflectSet(target: JsAbiValue, propertyKey: string, value: JsAbiValue): JsAbiResult; export function throwProhibitRewindException(message: string): void; diff --git a/packages/npm-packages/ruby-wasm-wasi/src/bindgen/interfaces/ruby-js-ruby-runtime.d.ts b/packages/npm-packages/ruby-wasm-wasi/src/bindgen/interfaces/ruby-js-ruby-runtime.d.ts index 9d3de7c245..899e91599d 100644 --- a/packages/npm-packages/ruby-wasm-wasi/src/bindgen/interfaces/ruby-js-ruby-runtime.d.ts +++ b/packages/npm-packages/ruby-wasm-wasi/src/bindgen/interfaces/ruby-js-ruby-runtime.d.ts @@ -1,12 +1,9 @@ export namespace RubyJsRubyRuntime { export function rubyShowVersion(): void; - export function rubyInit(): void; - export function rubySysinit(args: string[]): void; - export function rubyOptions(args: string[]): RbIseq; - export function rubyScript(name: string): void; + export function rubyInit(args: Array): void; export function rubyInitLoadpath(): void; export function rbEvalStringProtect(str: string): [RbAbiValue, number]; - export function rbFuncallvProtect(recv: RbAbiValue, mid: RbId, args: RbAbiValue[]): [RbAbiValue, number]; + export function rbFuncallvProtect(recv: RbAbiValue, mid: RbId, args: Array): [RbAbiValue, number]; export function rbIntern(name: string): RbId; export function rbErrinfo(): RbAbiValue; export function rbClearErrinfo(): void; @@ -27,7 +24,6 @@ export namespace RubyJsRubyRuntime { * 4. `export-rb-value-to-js()` returns the staged value */ export function exportRbValueToJs(): RbAbiValue; - export { RbIseq }; export { RbAbiValue }; } import type { JsAbiValue } from './ruby-js-js-runtime.js'; @@ -37,6 +33,3 @@ export type RbId = number; export class RbAbiValue { } - -export class RbIseq { -} diff --git a/packages/npm-packages/ruby-wasm-wasi/src/bindgen/legacy/rb-abi-guest.d.ts b/packages/npm-packages/ruby-wasm-wasi/src/bindgen/legacy/rb-abi-guest.d.ts index 9268312718..8bae01820a 100644 --- a/packages/npm-packages/ruby-wasm-wasi/src/bindgen/legacy/rb-abi-guest.d.ts +++ b/packages/npm-packages/ruby-wasm-wasi/src/bindgen/legacy/rb-abi-guest.d.ts @@ -68,10 +68,7 @@ export class RbAbiGuest { imports?: any, ): Promise; rubyShowVersion(): void; - rubyInit(): void; - rubySysinit(args: string[]): void; - rubyOptions(args: string[]): RbIseq; - rubyScript(name: string): void; + rubyInit(args: string[]): void; rubyInitLoadpath(): void; rbEvalStringProtect(str: string): [RbAbiValue, number]; rbFuncallvProtect(recv: RbAbiValue, mid: RbId, args: RbAbiValue[]): [RbAbiValue, number]; @@ -85,33 +82,6 @@ export class RbAbiGuest { rbSetShouldProhibitRewind(newValue: boolean): boolean; } -export class RbIseq { - // Creates a new strong reference count as a new - // object. This is only required if you're also - // calling `drop` below and want to manually manage - // the reference count from JS. - // - // If you don't call `drop`, you don't need to call - // this and can simply use the object from JS. - clone(): RbIseq; - - // Explicitly indicate that this JS object will no - // longer be used. If the internal reference count - // reaches zero then this will deterministically - // destroy the underlying wasm object. - // - // This is not required to be called from JS. Wasm - // destructors will be automatically called for you - // if this is not called using the JS - // `FinalizationRegistry`. - // - // Calling this method does not guarantee that the - // underlying wasm object is deallocated. Something - // else (including wasm) may be holding onto a - // strong reference count. - drop(): void; -} - export class RbAbiValue { // Creates a new strong reference count as a new // object. This is only required if you're also diff --git a/packages/npm-packages/ruby-wasm-wasi/src/bindgen/legacy/rb-abi-guest.js b/packages/npm-packages/ruby-wasm-wasi/src/bindgen/legacy/rb-abi-guest.js index 5919b716cd..78c7273241 100644 --- a/packages/npm-packages/ruby-wasm-wasi/src/bindgen/legacy/rb-abi-guest.js +++ b/packages/npm-packages/ruby-wasm-wasi/src/bindgen/legacy/rb-abi-guest.js @@ -2,39 +2,23 @@ import { data_view, to_uint32, UTF8_DECODER, utf8_encode, UTF8_ENCODED_LEN, Slab export class RbAbiGuest { constructor() { this._resource0_slab = new Slab(); - this._resource1_slab = new Slab(); } addToImports(imports) { if (!("canonical_abi" in imports)) imports["canonical_abi"] = {}; - imports.canonical_abi['resource_drop_rb-iseq'] = i => { + imports.canonical_abi['resource_drop_rb-abi-value'] = i => { this._resource0_slab.remove(i).drop(); }; - imports.canonical_abi['resource_clone_rb-iseq'] = i => { + imports.canonical_abi['resource_clone_rb-abi-value'] = i => { const obj = this._resource0_slab.get(i); return this._resource0_slab.insert(obj.clone()) }; - imports.canonical_abi['resource_get_rb-iseq'] = i => { - return this._resource0_slab.get(i)._wasm_val; - }; - imports.canonical_abi['resource_new_rb-iseq'] = i => { - const registry = this._registry0; - return this._resource0_slab.insert(new RbIseq(i, this)); - }; - - imports.canonical_abi['resource_drop_rb-abi-value'] = i => { - this._resource1_slab.remove(i).drop(); - }; - imports.canonical_abi['resource_clone_rb-abi-value'] = i => { - const obj = this._resource1_slab.get(i); - return this._resource1_slab.insert(obj.clone()) - }; imports.canonical_abi['resource_get_rb-abi-value'] = i => { - return this._resource1_slab.get(i)._wasm_val; + return this._resource0_slab.get(i)._wasm_val; }; imports.canonical_abi['resource_new_rb-abi-value'] = i => { - const registry = this._registry1; - return this._resource1_slab.insert(new RbAbiValue(i, this)); + const registry = this._registry0; + return this._resource0_slab.insert(new RbAbiValue(i, this)); }; } @@ -54,32 +38,12 @@ export class RbAbiGuest { this.instance = instance; } this._exports = this.instance.exports; - this._registry0 = new FinalizationRegistry(this._exports['canonical_abi_drop_rb-iseq']); - this._registry1 = new FinalizationRegistry(this._exports['canonical_abi_drop_rb-abi-value']); + this._registry0 = new FinalizationRegistry(this._exports['canonical_abi_drop_rb-abi-value']); } rubyShowVersion() { this._exports['ruby-show-version: func() -> ()'](); } - rubyInit() { - this._exports['ruby-init: func() -> ()'](); - } - rubySysinit(arg0) { - const memory = this._exports.memory; - const realloc = this._exports["cabi_realloc"]; - const vec1 = arg0; - const len1 = vec1.length; - const result1 = realloc(0, 0, 4, len1 * 8); - for (let i = 0; i < vec1.length; i++) { - const e = vec1[i]; - const base = result1 + i * 8; - const ptr0 = utf8_encode(e, realloc, memory); - const len0 = UTF8_ENCODED_LEN; - data_view(memory).setInt32(base + 4, len0, true); - data_view(memory).setInt32(base + 0, ptr0, true); - } - this._exports['ruby-sysinit: func(args: list) -> ()'](result1, len1); - } - rubyOptions(arg0) { + rubyInit(arg0) { const memory = this._exports.memory; const realloc = this._exports["cabi_realloc"]; const vec1 = arg0; @@ -93,15 +57,7 @@ export class RbAbiGuest { data_view(memory).setInt32(base + 4, len0, true); data_view(memory).setInt32(base + 0, ptr0, true); } - const ret = this._exports['ruby-options: func(args: list) -> handle'](result1, len1); - return this._resource0_slab.remove(ret); - } - rubyScript(arg0) { - const memory = this._exports.memory; - const realloc = this._exports["cabi_realloc"]; - const ptr0 = utf8_encode(arg0, realloc, memory); - const len0 = UTF8_ENCODED_LEN; - this._exports['ruby-script: func(name: string) -> ()'](ptr0, len0); + this._exports['ruby-init: func(args: list) -> ()'](result1, len1); } rubyInitLoadpath() { this._exports['ruby-init-loadpath: func() -> ()'](); @@ -112,7 +68,7 @@ export class RbAbiGuest { const ptr0 = utf8_encode(arg0, realloc, memory); const len0 = UTF8_ENCODED_LEN; const ret = this._exports['rb-eval-string-protect: func(str: string) -> tuple, s32>'](ptr0, len0); - return [this._resource1_slab.remove(data_view(memory).getInt32(ret + 0, true)), data_view(memory).getInt32(ret + 4, true)]; + return [this._resource0_slab.remove(data_view(memory).getInt32(ret + 0, true)), data_view(memory).getInt32(ret + 4, true)]; } rbFuncallvProtect(arg0, arg1, arg2) { const memory = this._exports.memory; @@ -127,10 +83,10 @@ export class RbAbiGuest { const base = result2 + i * 4; const obj1 = e; if (!(obj1 instanceof RbAbiValue)) throw new TypeError('expected instance of RbAbiValue'); - data_view(memory).setInt32(base + 0, this._resource1_slab.insert(obj1.clone()), true); + data_view(memory).setInt32(base + 0, this._resource0_slab.insert(obj1.clone()), true); } - const ret = this._exports['rb-funcallv-protect: func(recv: handle, mid: u32, args: list>) -> tuple, s32>'](this._resource1_slab.insert(obj0.clone()), to_uint32(arg1), result2, len2); - return [this._resource1_slab.remove(data_view(memory).getInt32(ret + 0, true)), data_view(memory).getInt32(ret + 4, true)]; + const ret = this._exports['rb-funcallv-protect: func(recv: handle, mid: u32, args: list>) -> tuple, s32>'](this._resource0_slab.insert(obj0.clone()), to_uint32(arg1), result2, len2); + return [this._resource0_slab.remove(data_view(memory).getInt32(ret + 0, true)), data_view(memory).getInt32(ret + 4, true)]; } rbIntern(arg0) { const memory = this._exports.memory; @@ -142,7 +98,7 @@ export class RbAbiGuest { } rbErrinfo() { const ret = this._exports['rb-errinfo: func() -> handle'](); - return this._resource1_slab.remove(ret); + return this._resource0_slab.remove(ret); } rbClearErrinfo() { this._exports['rb-clear-errinfo: func() -> ()'](); @@ -151,7 +107,7 @@ export class RbAbiGuest { const memory = this._exports.memory; const obj0 = arg0; if (!(obj0 instanceof RbAbiValue)) throw new TypeError('expected instance of RbAbiValue'); - const ret = this._exports['rstring-ptr: func(value: handle) -> string'](this._resource1_slab.insert(obj0.clone())); + const ret = this._exports['rstring-ptr: func(value: handle) -> string'](this._resource0_slab.insert(obj0.clone())); const ptr1 = data_view(memory).getInt32(ret + 0, true); const len1 = data_view(memory).getInt32(ret + 4, true); const result1 = UTF8_DECODER.decode(new Uint8Array(memory.buffer, ptr1, len1)); @@ -178,7 +134,7 @@ export class RbAbiGuest { } } -export class RbIseq { +export class RbAbiValue { constructor(wasm_val, obj) { this._wasm_val = wasm_val; this._obj = obj; @@ -196,33 +152,6 @@ export class RbIseq { if (this._refcnt !== 0) return; this._obj._registry0.unregister(this); - const dtor = this._obj._exports['canonical_abi_drop_rb-iseq']; - const wasm_val = this._wasm_val; - delete this._obj; - delete this._refcnt; - delete this._wasm_val; - dtor(wasm_val); - } -} - -export class RbAbiValue { - constructor(wasm_val, obj) { - this._wasm_val = wasm_val; - this._obj = obj; - this._refcnt = 1; - obj._registry1.register(this, wasm_val, this); - } - - clone() { - this._refcnt += 1; - return this; - } - - drop() { - this._refcnt -= 1; - if (this._refcnt !== 0) - return; - this._obj._registry1.unregister(this); const dtor = this._obj._exports['canonical_abi_drop_rb-abi-value']; const wasm_val = this._wasm_val; delete this._obj; diff --git a/packages/npm-packages/ruby-wasm-wasi/src/binding.ts b/packages/npm-packages/ruby-wasm-wasi/src/binding.ts index 96df7ba4cc..6ae5dae8fb 100644 --- a/packages/npm-packages/ruby-wasm-wasi/src/binding.ts +++ b/packages/npm-packages/ruby-wasm-wasi/src/binding.ts @@ -7,10 +7,7 @@ import * as RbAbi from "./bindgen/legacy/rb-abi-guest.js"; */ export interface Binding { rubyShowVersion(): void; - rubyInit(): void; - rubySysinit(args: string[]): void; - rubyOptions(args: string[]): void; - rubyScript(name: string): void; + rubyInit(args: string[]): void; rubyInitLoadpath(): void; rbEvalStringProtect(str: string): [RbAbiValue, number]; rbFuncallvProtect(recv: RbAbiValue, mid: RbAbi.RbId, args: RbAbiValue[]): [RbAbiValue, number]; @@ -48,17 +45,8 @@ export class ComponentBinding implements Binding { rubyShowVersion(): void { this.underlying.rubyShowVersion(); } - rubyInit(): void { - this.underlying.rubyInit(); - } - rubySysinit(args: string[]): void { - this.underlying.rubySysinit(args); - } - rubyOptions(args: string[]) { - this.underlying.rubyOptions(args); - } - rubyScript(name: string): void { - this.underlying.rubyScript(name); + rubyInit(args: string[]): void { + this.underlying.rubyInit(args); } rubyInitLoadpath(): void { this.underlying.rubyInitLoadpath(); diff --git a/packages/npm-packages/ruby-wasm-wasi/src/browser.ts b/packages/npm-packages/ruby-wasm-wasi/src/browser.ts index 5c6bed680f..8d29dd02e9 100644 --- a/packages/npm-packages/ruby-wasm-wasi/src/browser.ts +++ b/packages/npm-packages/ruby-wasm-wasi/src/browser.ts @@ -25,22 +25,16 @@ export const DefaultRubyVM = async ( new PreopenDirectory("/", new Map()), ]; const wasi = new WASI(args, env, fds, { debug: false }); - const vm = new RubyVM(); - - const imports = { - wasi_snapshot_preview1: wasi.wasiImport, - }; - vm.addToImports(imports); const printer = options.consolePrint ?? true ? consolePrinter() : undefined; - printer?.addToImports(imports); - - const instance = await WebAssembly.instantiate(rubyModule, imports); - await vm.setInstance(instance); - - printer?.setMemory(instance.exports.memory as WebAssembly.Memory); - - wasi.initialize(instance as any); - vm.initialize(); + const { vm, instance } = await RubyVM.instantiateModule({ + module: rubyModule, wasip1: wasi, + addToImports: (imports) => { + printer?.addToImports(imports); + }, + setMemory: (memory) => { + printer?.setMemory(memory); + } + }); return { vm, diff --git a/packages/npm-packages/ruby-wasm-wasi/src/node.ts b/packages/npm-packages/ruby-wasm-wasi/src/node.ts index 25727b5d4a..2348510b3a 100644 --- a/packages/npm-packages/ruby-wasm-wasi/src/node.ts +++ b/packages/npm-packages/ruby-wasm-wasi/src/node.ts @@ -6,19 +6,7 @@ export const DefaultRubyVM = async ( options: { env?: Record | undefined } = {}, ) => { const wasi = new WASI({ env: options.env, version: "preview1", returnOnExit: true }); - const vm = new RubyVM(); - const imports = { - wasi_snapshot_preview1: wasi.wasiImport, - }; - - vm.addToImports(imports); - - const instance = await WebAssembly.instantiate(rubyModule, imports); - - await vm.setInstance(instance); - - wasi.initialize(instance); - vm.initialize(); + const { vm, instance } = await RubyVM.instantiateModule({ module: rubyModule, wasip1: wasi }); return { vm, diff --git a/packages/npm-packages/ruby-wasm-wasi/src/vm.ts b/packages/npm-packages/ruby-wasm-wasi/src/vm.ts index c7f723fadd..efd3d39499 100644 --- a/packages/npm-packages/ruby-wasm-wasi/src/vm.ts +++ b/packages/npm-packages/ruby-wasm-wasi/src/vm.ts @@ -9,26 +9,92 @@ import { } from "./bindgen/legacy/rb-js-abi-host.js"; import { Binding, ComponentBinding, LegacyBinding, RbAbiValue } from "./binding.js"; +export type RubyInitComponentOptions = { + /** + * A lower-level instantiation function that instantiates the Ruby component with the given component + * that implements "ruby:js/js-runtime" WIT interface. + */ + instantiate: (_: typeof RubyJsJsRuntime) => Promise; + + /** + * The arguments to pass to the Ruby VM. Note that the first argument must be the Ruby program name. + * + * @default ["ruby.wasm", "-EUTF-8", "-e_=0"] + */ + args?: string[]; +} | { + /** + * An `instantiate` function generated by `@bytecodealliance/jco` that instantiates the Ruby component. + */ + instantiate: ( + ( + getCoreModule: (path: string) => WebAssembly.Module, + importObject: any, + instantiateCore?: (module: WebAssembly.Module, imports: Record) => WebAssembly.Instance | Promise, + ) => Promise<({ rubyRuntime: typeof RubyJsRubyRuntime })> + ), + + /** + * A function that returns a WebAssembly Core module within the Ruby component transpiled by `@bytecodealliance/jco`. + */ + getCoreModule: (path: string) => Promise, + + /** + * An optional function used to instantiate a WebAssembly Core module + */ + instantiateCore?: (module: WebAssembly.Module, imports: Record) => WebAssembly.Instance | Promise + + /** + * WASI Preview 2 implementation, typically imported from `import * as wasip2 from "@bytecodealliance/preview2-shim"` + */ + wasip2: any; + + /** + * The arguments to pass to the Ruby VM. Note that the first argument must be the Ruby program name. + * + * @default ["ruby.wasm", "-EUTF-8", "-e_=0"] + */ + args?: string[]; +} + +export type RubyInitModuleOptions = { + /** + * The WebAssembly module that contains the Ruby VM + */ + module: WebAssembly.Module; + /** + * WASI Preview 1 implementation supporting reactor model ABI + */ + wasip1: { + wasiImport: WebAssembly.ModuleImports; + initialize(instance: WebAssembly.Instance): void; + }; + /** + * The arguments to pass to the Ruby VM. Note that the first argument must be the Ruby program name. + * @default ["ruby.wasm", "-EUTF-8", "-e_=0"] + */ + args?: string[]; + /** + * A hook to add additional imports to the WebAssembly instance + */ + addToImports?: (imports: WebAssembly.Imports) => void; + /** + * A hook called with the WebAssembly memory instance just after the Ruby VM is instantiated + */ + setMemory?: (memory: WebAssembly.Memory) => void; +} + +export type RubyInitOptions = RubyInitComponentOptions | RubyInitModuleOptions; + /** * A Ruby VM instance - * - * @example - * - * const wasi = new WASI(); - * const vm = new RubyVM(); - * const imports = { - * wasi_snapshot_preview1: wasi.wasiImport, - * }; - * - * vm.addToImports(imports); - * - * const instance = await WebAssembly.instantiate(rubyModule, imports); - * await vm.setInstance(instance); - * wasi.initialize(instance); - * vm.initialize(); - * + * @see {@link RubyVM.instantiateComponent} and {@link RubyVM.instantiateModule} to create a new instance + * @category Essentials */ export class RubyVM { + /** + * @private Only for internal use. + */ guest: Binding; private instance: WebAssembly.Instance | null = null; private transport: JsValueTransport; @@ -37,6 +103,136 @@ export class RubyVM { hasJSFrameAfterRbFrame: false, }; + /** + * Instantiate a Ruby VM with the given WebAssembly Core module with WASI Preview 1 implementation. + * + * @param options The options to instantiate the Ruby VM + * @returns A promise that resolves to the Ruby VM instance and the WebAssembly instance + * @category Essentials + * + * @example + * + * import { WASI } from "@bjorn3/browser_wasi_shim"; + * const wasip1 = new WASI([], [], []); + * const module = await WebAssembly.compile("./path/to/ruby.wasm"); + * const { vm } = await RubyVM.instantiateModule({ module, wasip1 }); + * + */ + static async instantiateModule(options: RubyInitModuleOptions): Promise<{ vm: RubyVM, instance: WebAssembly.Instance }> { + const { module, wasip1 } = options; + const vm = new RubyVM(); + const imports: WebAssembly.Imports = { + wasi_snapshot_preview1: wasip1.wasiImport, + }; + vm.addToImports(imports); + options.addToImports?.(imports); + const instance = await WebAssembly.instantiate(module, imports); + await vm.setInstance(instance); + options.setMemory?.(instance.exports.memory as WebAssembly.Memory); + wasip1.initialize(instance); + vm.initialize(options.args); + return { vm, instance }; + } + + /** + * Instantiate a Ruby VM with the given WebAssembly component with WASI Preview 2 implementation. + * + * @param options The options to instantiate the Ruby VM + * @returns A promise that resolves to the Ruby VM instance + * @category Essentials + * + * @example + * + * // First, you need to transpile the Ruby component to a JavaScript module using jco. + * // $ jco transpile --no-wasi-shim --instantiation --valid-lifting-optimization ./ruby.component.wasm -o ./component + * // Then, you can instantiate the Ruby VM with the component: + * + * import * as wasip2 from "@bytecodealliance/preview2-shim" + * import fs from "fs/promises"; + * import path from "path"; + * + * const { instantiate } = await import("./component/ruby.component.js"); + * const getCoreModule = async (relativePath) => { + * const buffer = await fs.readFile(path.join("./component", relativePath)); + * return WebAssembly.compile(buffer); + * } + * + * const { vm } = await RubyVM.instantiateComponent({ + * instantiate, getCoreModule, wasip2, + * }); + * + */ + static async instantiateComponent(options: RubyInitComponentOptions): Promise<{ vm: RubyVM }> { + let initComponent: (_: typeof RubyJsJsRuntime) => Promise; + if ("getCoreModule" in options) { + // A convenience overload to instantiate with "instantiate" function generated by jco + initComponent = async (jsRuntime) => { + const { instantiate, getCoreModule, wasip2 } = options; + const { cli, clocks, filesystem, io, random, sockets, http } = wasip2; + const importObject = { + "ruby:js/js-runtime": jsRuntime, + "wasi:cli/environment": cli.environment, + "wasi:cli/exit": cli.exit, + "wasi:cli/stderr": cli.stderr, + "wasi:cli/stdin": cli.stdin, + "wasi:cli/stdout": cli.stdout, + "wasi:cli/terminal-input": cli.terminalInput, + "wasi:cli/terminal-output": cli.terminalOutput, + "wasi:cli/terminal-stderr": cli.terminalStderr, + "wasi:cli/terminal-stdin": cli.terminalStdin, + "wasi:cli/terminal-stdout": cli.terminalStdout, + "wasi:clocks/monotonic-clock": clocks.monotonicClock, + "wasi:clocks/wall-clock": clocks.wallClock, + "wasi:filesystem/preopens": filesystem.preopens, + "wasi:filesystem/types": filesystem.types, + "wasi:io/error": io.error, + "wasi:io/poll": io.poll, + "wasi:io/streams": io.streams, + "wasi:random/random": random.random, + "wasi:sockets/tcp": sockets.tcp, + "wasi:http/types": http.types, + "wasi:http/incoming-handler": http.incomingHandler, + "wasi:http/outgoing-handler": http.outgoingHandler, + }; + const component = await instantiate(getCoreModule, importObject, options.instantiateCore); + return component.rubyRuntime; + } + } else { + initComponent = options.instantiate; + } + const vm = await this._instantiate({}, initComponent); + return { vm }; + } + + /** + * Create a new Ruby VM instance with low-level initialization control. + * + * @category Low-level initialization + * @example + * + * ```javascript + * const wasi = new WASI(); + * const vm = new RubyVM(); + * const imports = { + * wasi_snapshot_preview1: wasi.wasiImport, + * }; + * + * vm.addToImports(imports); + * + * const instance = await WebAssembly.instantiate(rubyModule, imports); + * await vm.setInstance(instance); + * wasi.initialize(instance); + * vm.initialize(); + * ``` + * + */ + constructor(); + + /** + * @private Only for internal use. + */ + constructor(binding?: Binding); + constructor(binding?: Binding) { // Wrap exported functions from Ruby VM to prohibit nested VM operation // if the call stack has sandwitched JS frames like JS -> Ruby -> JS -> Ruby. @@ -84,9 +280,10 @@ export class RubyVM { this.exceptionFormatter = new RbExceptionFormatter(); } - static async _instantiate(initComponent: (_: typeof RubyJsJsRuntime) => Promise, options: { - args?: string[], - }): Promise { + private static async _instantiate( + options: { args?: string[] }, + initComponent: (_: typeof RubyJsJsRuntime) => Promise + ): Promise { const binding = new ComponentBinding() const vm = new RubyVM(binding); class JsAbiValue { @@ -119,12 +316,11 @@ export class RubyVM { * Initialize the Ruby VM with the given command line arguments * @param args The command line arguments to pass to Ruby. Must be * an array of strings starting with the Ruby program name. + * @category Low-level initialization */ initialize(args: string[] = ["ruby.wasm", "-EUTF-8", "-e_=0"]) { const c_args = args.map((arg) => arg + "\0"); - this.guest.rubyInit(); - this.guest.rubySysinit(c_args); - this.guest.rubyOptions(c_args); + this.guest.rubyInit(c_args); try { this.eval(` # Require Bundler standalone setup @@ -148,6 +344,7 @@ export class RubyVM { * @param instance The WebAssembly instance to interact with. Must * be instantiated from a Ruby built with JS extension, and built * with Reactor ABI instead of command line. + * @category Low-level initialization */ async setInstance(instance: WebAssembly.Instance) { this.instance = instance; @@ -158,6 +355,7 @@ export class RubyVM { * Add intrinsic import entries, which is necessary to interact JavaScript * and Ruby's WebAssembly instance. * @param imports The import object to add to the WebAssembly instance + * @category Low-level initialization */ addToImports(imports: WebAssembly.Imports) { this.guest.addToImports(imports); @@ -373,6 +571,7 @@ export class RubyVM { * Runs a string of Ruby code from JavaScript * @param code The Ruby code to run * @returns the result of the last expression + * @category Essentials * * @example * vm.eval("puts 'hello world'"); @@ -389,6 +588,7 @@ export class RubyVM { * Returns a promise that resolves when execution completes. * @param code The Ruby code to run * @returns a promise that resolves to the result of the last expression + * @category Essentials * * @example * const text = await vm.evalAsync(` @@ -484,6 +684,7 @@ class JsValueTransport { /** * A RbValue is an object that represents a value in Ruby + * @category Essentials */ export class RbValue { /** diff --git a/packages/npm-packages/ruby-wasm-wasi/test/init.js b/packages/npm-packages/ruby-wasm-wasi/test/init.js index ef6e2a206a..c55c4b95e9 100644 --- a/packages/npm-packages/ruby-wasm-wasi/test/init.js +++ b/packages/npm-packages/ruby-wasm-wasi/test/init.js @@ -38,19 +38,8 @@ const initModuleRubyVM = async ({ suppressStderr } = { suppressStderr: false }) preopens, }); - const vm = new RubyVM(); - const imports = { - wasi_snapshot_preview1: wasi.wasiImport, - }; - - vm.addToImports(imports); - - const instance = await WebAssembly.instantiate(await rubyModule, imports); - await vm.setInstance(instance); - - wasi.initialize(instance); - - vm.initialize(); + const module = await rubyModule; + const { vm } = await RubyVM.instantiateModule({ module, wasip1: wasi }) return vm; }; @@ -72,43 +61,19 @@ async function initComponentRubyVM({ suppressStderr } = { suppressStderr: false moduleCache.set(coreModulePath, module); return module; } - const vm = await RubyVM._instantiate(async (jsRuntime) => { - const { cli, clocks, filesystem, io, random, sockets, http } = preview2Shim; - if (process.env.RUBY_BUILD_ROOT) { - filesystem._setPreopens({ - "/usr": path.join(process.env.RUBY_BUILD_ROOT, "usr"), - "/bundle": path.join(process.env.RUBY_BUILD_ROOT, "bundle"), - }) - } - cli._setArgs(["ruby.wasm"].concat(process.argv.slice(2))); - cli._setCwd("/") - const root = await instantiate(getCoreModule, { - "ruby:js/js-runtime": jsRuntime, - "wasi:cli/environment": cli.environment, - "wasi:cli/exit": cli.exit, - "wasi:cli/stderr": cli.stderr, - "wasi:cli/stdin": cli.stdin, - "wasi:cli/stdout": cli.stdout, - "wasi:cli/terminal-input": cli.terminalInput, - "wasi:cli/terminal-output": cli.terminalOutput, - "wasi:cli/terminal-stderr": cli.terminalStderr, - "wasi:cli/terminal-stdin": cli.terminalStdin, - "wasi:cli/terminal-stdout": cli.terminalStdout, - "wasi:clocks/monotonic-clock": clocks.monotonicClock, - "wasi:clocks/wall-clock": clocks.wallClock, - "wasi:filesystem/preopens": filesystem.preopens, - "wasi:filesystem/types": filesystem.types, - "wasi:io/error": io.error, - "wasi:io/poll": io.poll, - "wasi:io/streams": io.streams, - "wasi:random/random": random.random, - "wasi:sockets/tcp": sockets.tcp, - "wasi:http/types": http.types, - "wasi:http/incoming-handler": http.incomingHandler, - "wasi:http/outgoing-handler": http.outgoingHandler, + + const { cli, clocks, filesystem, io, random, sockets, http } = preview2Shim; + if (process.env.RUBY_BUILD_ROOT) { + filesystem._setPreopens({ + "/usr": path.join(process.env.RUBY_BUILD_ROOT, "usr"), + "/bundle": path.join(process.env.RUBY_BUILD_ROOT, "bundle"), }) - return root.rubyRuntime; - }, {}) + } + cli._setArgs(["ruby.wasm"].concat(process.argv.slice(2))); + cli._setCwd("/") + const { vm } = await RubyVM.instantiateComponent({ + instantiate, getCoreModule, wasip2: preview2Shim, + }) return vm; } diff --git a/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs b/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs index f188fb5a8d..70d8ec06f6 100755 --- a/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs +++ b/packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs @@ -34,45 +34,19 @@ const instantiateComponent = async (rootTestFile) => { const buffer = await fs.readFile(coreModulePath); return WebAssembly.compile(buffer); } - const vm = await RubyVM._instantiate(async (jsRuntime) => { - const { cli, clocks, filesystem, io, random, sockets, http } = preview2Shim; - const dirname = path.dirname(new URL(import.meta.url).pathname); - const preopens = { "/__root__": path.join(dirname, "..") }; - if (process.env.RUBY_ROOT) { - preopens["/usr"] = path.join(process.env.RUBY_ROOT, "usr"); - preopens["/bundle"] = path.join(process.env.RUBY_ROOT, "bundle"); - } - filesystem._setPreopens(preopens); - cli._setArgs(["ruby.wasm"].concat(process.argv.slice(2))); - cli._setCwd("/") - const root = await instantiate(getCoreModule, { - "ruby:js/js-runtime": jsRuntime, - "wasi:cli/environment": cli.environment, - "wasi:cli/exit": cli.exit, - "wasi:cli/stderr": cli.stderr, - "wasi:cli/stdin": cli.stdin, - "wasi:cli/stdout": cli.stdout, - "wasi:cli/terminal-input": cli.terminalInput, - "wasi:cli/terminal-output": cli.terminalOutput, - "wasi:cli/terminal-stderr": cli.terminalStderr, - "wasi:cli/terminal-stdin": cli.terminalStdin, - "wasi:cli/terminal-stdout": cli.terminalStdout, - "wasi:clocks/monotonic-clock": clocks.monotonicClock, - "wasi:clocks/wall-clock": clocks.wallClock, - "wasi:filesystem/preopens": filesystem.preopens, - "wasi:filesystem/types": filesystem.types, - "wasi:io/error": io.error, - "wasi:io/poll": io.poll, - "wasi:io/streams": io.streams, - "wasi:random/random": random.random, - "wasi:sockets/tcp": sockets.tcp, - "wasi:http/types": http.types, - "wasi:http/incoming-handler": http.incomingHandler, - "wasi:http/outgoing-handler": http.outgoingHandler, - }) - return root.rubyRuntime; - }, { - args: ["ruby.wasm", rootTestFile], + + const { cli, filesystem } = preview2Shim; + const dirname = path.dirname(new URL(import.meta.url).pathname); + const preopens = { "/__root__": path.join(dirname, "..") }; + if (process.env.RUBY_ROOT) { + preopens["/usr"] = path.join(process.env.RUBY_ROOT, "usr"); + preopens["/bundle"] = path.join(process.env.RUBY_ROOT, "bundle"); + } + filesystem._setPreopens(preopens); + cli._setArgs(["ruby.wasm", rootTestFile]); + cli._setCwd("/") + const { vm } = await RubyVM.instantiateComponent({ + instantiate, getCoreModule, wasip2: preview2Shim, }) return { vm }; } @@ -85,7 +59,7 @@ const instantiateNodeWasi = async (rootTestFile) => { const rubyModule = await WebAssembly.compile(binary); const wasi = new nodeWasi.WASI({ stdio: "inherit", - args: ["ruby.wasm"].concat(process.argv.slice(2)), + args: ["ruby.wasm", rootTestFile], env: { ...process.env, // Extend fiber stack size to be able to run test-unit @@ -95,20 +69,10 @@ const instantiateNodeWasi = async (rootTestFile) => { version: "preview1", }); - const vm = new RubyVM(); - const imports = { - wasi_snapshot_preview1: wasi.wasiImport, - }; - - vm.addToImports(imports); - - const instance = await WebAssembly.instantiate(rubyModule, imports); - await vm.setInstance(instance); - - wasi.initialize(instance); - - vm.initialize(["ruby.wasm", rootTestFile]); - return { instance, vm, wasi }; + const { vm } = await RubyVM.instantiateModule({ + module: rubyModule, wasip1: wasi, + }) + return { vm, wasi }; }; const instantiateBrowserWasi = async (rootTestFile) => { @@ -181,31 +145,26 @@ const instantiateBrowserWasi = async (rootTestFile) => { const wasi = new browserWasi.WASI(args, env, fds, { debug: false, }); - - const vm = new RubyVM(); - const imports = { - wasi_snapshot_preview1: wasi.wasiImport, - }; const printer = consolePrinter({ stdout: (str) => process.stdout.write(str), stderr: (str) => process.stderr.write(str), }); - printer.addToImports(imports); - vm.addToImports(imports); - const instance = await WebAssembly.instantiate(rubyModule, imports); - printer.setMemory(instance.exports.memory); - await vm.setInstance(instance); - - wasi.initialize(instance); - vm.initialize(["ruby.wasm", rootTestFile]); - - return { instance, vm, wasi }; + const { vm } = await RubyVM.instantiateModule({ + module: rubyModule, wasip1: wasi, + addToImports: (imports) => { + printer.addToImports(imports); + }, + setMemory: (memory) => { + printer.setMemory(memory); + } + }); + return { vm, wasi }; }; const test = async (instantiate) => { const rootTestFile = "/__root__/test/test_unit.rb"; - const { vm, wasi } = await instantiate(rootTestFile); + const { vm } = await instantiate(rootTestFile); await vm.evalAsync(` diff --git a/rakelib/check.rake b/rakelib/check.rake index 6282b2a009..61a912082a 100644 --- a/rakelib/check.rake +++ b/rakelib/check.rake @@ -41,8 +41,12 @@ namespace :check do ) end + task bindgen_ts: :install_wit_bindgen do + sh "node", "packages/npm-packages/ruby-wasm-wasi/tools/component-ts-bindgen.mjs" + end + desc "Check wit-bindgen'ed sources are up-to-date" - task bindgen: %i[bindgen_c legacy_bindgen_c legacy_bindgen_js] + task bindgen: %i[bindgen_c bindgen_ts legacy_bindgen_c legacy_bindgen_js] task :type do sh "bundle exec steep check" diff --git a/rakelib/packaging.rake b/rakelib/packaging.rake index edbb150d96..dfdccd7206 100644 --- a/rakelib/packaging.rake +++ b/rakelib/packaging.rake @@ -48,6 +48,7 @@ def vendor_gem_cache(pkg) require_relative "../packages/gems/js/lib/js/version" sh "gem", "-C", "packages/gems/js", "build", "-o", File.join(vendor_cache_dir, "js-#{JS::VERSION}.gem") + JS::VERSION end namespace :npm do @@ -62,7 +63,7 @@ namespace :npm do # Skip if the package does not require building ruby next unless build_command - vendor_gem_cache(pkg) + js_gem_version = vendor_gem_cache(pkg) env = { # Share ./build and ./rubies in the same workspace @@ -80,12 +81,15 @@ namespace :npm do mkdir_p dist_dir if pkg[:target].start_with?("wasm32-unknown-wasi") Dir.chdir(cwd || base_dir) do + # Uninstall js gem to re-install just-built js gem + sh "gem", "uninstall", "js", "-v", js_gem_version, "--force" + # Install gems including js gem sh "bundle", "install" sh env, "bundle", "exec", *build_command, - "--no-stdlib", + "--no-stdlib", "--remake", "-o", File.join(dist_dir, "ruby.wasm") sh env, @@ -97,6 +101,9 @@ namespace :npm do component_path = File.join(pkg_dir, "tmp", "ruby.component.wasm") FileUtils.mkdir_p(File.dirname(component_path)) + # Remove js gem from the ./bundle directory to force Bundler to re-install it + rm_rf FileList[File.join(pkg_dir, "bundle", "**", "js-#{js_gem_version}")] + sh env.merge("RUBY_WASM_EXPERIMENTAL_DYNAMIC_LINKING" => "1"), *build_command, "-o", component_path sh "npx", "jco", "transpile",