Skip to content

Package Componentized Ruby in npm package #437

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ NPM_PACKAGES = [
name: "ruby-head-wasm-wasi",
ruby_version: "head",
gemfile: "packages/npm-packages/ruby-wasm-wasi/Gemfile",
target: "wasm32-unknown-wasip1"
target: "wasm32-unknown-wasip1",
enable_component_model: true,
},
{
name: "ruby-3.3-wasm-wasi",
Expand Down
6 changes: 6 additions & 0 deletions lib/ruby_wasm/packager/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ def build(executor, options)

def cache_key(digest)
derive_build.cache_key(digest)
if enabled = @packager.features.support_component_model?
digest << enabled.to_s
end
end

def artifact
Expand Down Expand Up @@ -340,6 +343,9 @@ def name
exts = specs_with_extensions.sort
hash = ::Digest::MD5.new
specs_with_extensions.each { |spec, _| hash << spec.full_name }
if enabled = @packager.features.support_component_model?
hash << enabled.to_s
end
exts.empty? ? base : "#{base}-#{hash.hexdigest}"
end
end
Expand Down
101 changes: 101 additions & 0 deletions packages/npm-packages/ruby-wasm-wasi/src/binding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { RubyJsRubyRuntime } from "./bindgen/interfaces/ruby-js-ruby-runtime.js";
import * as RbAbi from "./bindgen/legacy/rb-abi-guest.js";

/**
* This interface bridges between the Ruby runtime and the JavaScript runtime
* and defines how to interact with underlying import/export functions.
*/
export interface Binding {
rubyShowVersion(): void;
rubyInit(): void;
rubySysinit(args: string[]): void;
rubyOptions(args: string[]): void;
rubyScript(name: string): void;
rubyInitLoadpath(): void;
rbEvalStringProtect(str: string): [RbAbiValue, number];
rbFuncallvProtect(recv: RbAbiValue, mid: RbAbi.RbId, args: RbAbiValue[]): [RbAbiValue, number];
rbIntern(name: string): RbAbi.RbId;
rbErrinfo(): RbAbiValue;
rbClearErrinfo(): void;
rstringPtr(value: RbAbiValue): string;
rbVmBugreport(): void;
rbGcEnable(): boolean;
rbGcDisable(): boolean;
rbSetShouldProhibitRewind(newValue: boolean): boolean;

setInstance(instance: WebAssembly.Instance): Promise<void>;
addToImports(imports: WebAssembly.Imports): void;
}

// Low-level opaque representation of a Ruby value.
export interface RbAbiValue {}

export class LegacyBinding extends RbAbi.RbAbiGuest implements Binding {
async setInstance(instance: WebAssembly.Instance): Promise<void> {
await this.instantiate(instance);
}
}

export class ComponentBinding implements Binding {
underlying: typeof RubyJsRubyRuntime;

constructor(underlying: typeof RubyJsRubyRuntime) {
this.underlying = underlying;
}

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);
}
rubyInitLoadpath(): void {
this.underlying.rubyInitLoadpath();
}
rbEvalStringProtect(str: string): [RbAbiValue, number] {
return this.underlying.rbEvalStringProtect(str);
}
rbFuncallvProtect(recv: RbAbiValue, mid: number, args: RbAbiValue[]): [RbAbiValue, number] {
return this.rbFuncallvProtect(recv, mid, args);
}
rbIntern(name: string): number {
return this.rbIntern(name);
}
rbErrinfo(): RbAbi.RbAbiValue {
return this.rbErrinfo();
}
rbClearErrinfo(): void {
return this.rbClearErrinfo();
}
rstringPtr(value: RbAbi.RbAbiValue): string {
return this.rstringPtr(value);
}
rbVmBugreport(): void {
this.rbVmBugreport();
}
rbGcEnable(): boolean {
return this.rbGcEnable();
}
rbGcDisable(): boolean {
return this.rbGcDisable();
}
rbSetShouldProhibitRewind(newValue: boolean): boolean {
return this.rbSetShouldProhibitRewind(newValue);
}

async setInstance(instance: WebAssembly.Instance): Promise<void> {
// No-op
}
addToImports(imports: WebAssembly.Imports): void {
// No-op
}
}
26 changes: 17 additions & 9 deletions packages/npm-packages/ruby-wasm-wasi/src/vm.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { RubyJsRubyRuntime } from "./bindgen/interfaces/ruby-js-ruby-runtime.js";
import * as RbAbi from "./bindgen/legacy/rb-abi-guest.js";
import {
RbJsAbiHost,
addRbJsAbiHostToImports,
JsAbiResult,
JsAbiValue,
} from "./bindgen/legacy/rb-js-abi-host.js";
import { Binding, ComponentBinding, LegacyBinding, RbAbiValue } from "./binding.js";

/**
* A Ruby VM instance
Expand All @@ -26,19 +28,20 @@ import {
*
*/
export class RubyVM {
guest: RbAbi.RbAbiGuest;
guest: Binding;
private instance: WebAssembly.Instance | null = null;
private transport: JsValueTransport;
private exceptionFormatter: RbExceptionFormatter;
private interfaceState: RbAbiInterfaceState = {
hasJSFrameAfterRbFrame: false,
};

constructor() {
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.
const proxyExports = (exports: RbAbi.RbAbiGuest) => {
const excludedMethods: (keyof RbAbi.RbAbiGuest)[] = [
const proxyExports = (exports: Binding) => {
const excludedMethods: (keyof LegacyBinding | keyof Binding)[] = [
"setInstance",
"addToImports",
"instantiate",
"rbSetShouldProhibitRewind",
Expand Down Expand Up @@ -75,11 +78,16 @@ export class RubyVM {
}
return exports;
};
this.guest = proxyExports(new RbAbi.RbAbiGuest());
this.guest = proxyExports(binding ?? new LegacyBinding());
this.transport = new JsValueTransport();
this.exceptionFormatter = new RbExceptionFormatter();
}

static _instantiate(component: typeof RubyJsRubyRuntime): RubyVM {
const binding = new ComponentBinding(component)
return new RubyVM(binding);
}

/**
* Initialize the Ruby VM with the given command line arguments
* @param args The command line arguments to pass to Ruby. Must be
Expand All @@ -104,7 +112,7 @@ export class RubyVM {
*/
async setInstance(instance: WebAssembly.Instance) {
this.instance = instance;
await this.guest.instantiate(instance);
await this.guest.setInstance(instance);
}

/**
Expand Down Expand Up @@ -431,7 +439,7 @@ export class RbValue {
* @hideconstructor
*/
constructor(
private inner: RbAbi.RbAbiValue,
private inner: RbAbiValue,
private vm: RubyVM,
private privateObject: RubyVMPrivate,
) {}
Expand Down Expand Up @@ -702,9 +710,9 @@ function wrapRbOperation<R>(vm: RubyVM, body: () => R): R {
const callRbMethod = (
vm: RubyVM,
privateObject: RubyVMPrivate,
recv: RbAbi.RbAbiValue,
recv: RbAbiValue,
callee: string,
args: RbAbi.RbAbiValue[],
args: RbAbiValue[],
) => {
const mid = vm.guest.rbIntern(callee + "\0");
return wrapRbOperation(vm, () => {
Expand Down
8 changes: 8 additions & 0 deletions rakelib/packaging.rake
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ namespace :npm do
*build_command,
"-o",
File.join(dist_dir, "ruby.debug+stdlib.wasm")
if pkg[:enable_component_model]
component_path = File.join(dist_dir, "ruby.component.wasm")
sh env.merge("RUBY_WASM_EXPERIMENTAL_COMPONENT_MODEL" => "1"),
*build_command, "-o", component_path
sh "npx", "jco", "transpile",
"--no-wasi-shim", "--instantiation", "--valid-lifting-optimization", "--tracing",
component_path, "-o", File.join(dist_dir, "component")
end
end
sh wasi_sdk.wasm_opt,
"--strip-debug",
Expand Down