Skip to content

Allow wrapping Ruby and JS object to each other #18

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 5 commits into from
May 20, 2022
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
4 changes: 3 additions & 1 deletion ext/js/bindgen/rb-js-abi-host.wit
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ global-this: function() -> js-abi-value
int-to-js-number: function(value: s32) -> js-abi-value
string-to-js-string: function(value: string) -> js-abi-value
bool-to-js-bool: function(value: bool) -> js-abi-value
rb-object-to-js-rb-value: function(raw-rb-abi-value: u32) -> js-abi-value

js-value-to-string: function(value: js-abi-value) -> string

take-js-value: function(value: js-abi-value)
export-js-value-to-host: function(value: js-abi-value)
import-js-value-from-host: function() -> js-abi-value

reflect-apply: function(target: js-abi-value, this-argument: js-abi-value, arguments: list<js-abi-value>) -> js-abi-value
reflect-construct: function(target: js-abi-value, arguments: list<js-abi-value>) -> js-abi-value
Expand Down
25 changes: 21 additions & 4 deletions ext/js/js-core.c
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,10 @@ static VALUE _rb_js_is_js(VALUE _, VALUE obj) {
* Try to convert the given object to a JS::Object using <code>to_js</code>
* method. Returns <code>nil</code> if the object cannot be converted.
*
* p JS.try_convert(1) # => 1
* p JS.try_convert("foo") # => "foo"
* p JS.try_convert(Object.new) # => nil
* p JS.try_convert(1) # => JS::Object
* p JS.try_convert("foo") # => JS::Object
* p JS.try_convert(Object.new) # => nil
* p JS.try_convert(JS::Object.wrap(Object.new)) # => JS::Object
*/
VALUE _rb_js_try_convert(VALUE klass, VALUE obj) {
if (_rb_js_is_js(klass, obj)) {
Expand Down Expand Up @@ -238,10 +239,24 @@ static VALUE _rb_js_obj_inspect(VALUE obj) {
*/
static VALUE _rb_js_export_to_js(VALUE obj) {
struct jsvalue *p = check_jsvalue(obj);
rb_js_abi_host_take_js_value(p->abi);
rb_js_abi_host_export_js_value_to_host(p->abi);
return Qnil;
}

static VALUE _rb_js_import_from_js(VALUE obj) {
return jsvalue_s_new(rb_js_abi_host_import_js_value_from_host());
}

/*
* call-seq:
* JS::Object.wrap(obj) -> JS::Object
*
* Returns +obj+ wrapped by JS class RbValue.
*/
static VALUE _rb_js_obj_wrap(VALUE obj, VALUE wrapping) {
return jsvalue_s_new(rb_js_abi_host_rb_object_to_js_rb_value((uint32_t)wrapping));
}

/*
* call-seq:
* to_js -> JS::Object
Expand Down Expand Up @@ -305,7 +320,9 @@ void Init_js() {
rb_define_method(rb_cJS_Object, "[]=", _rb_js_obj_aset, 2);
rb_define_method(rb_cJS_Object, "call", _rb_js_obj_call, -1);
rb_define_method(rb_cJS_Object, "__export_to_js", _rb_js_export_to_js, 0);
rb_define_singleton_method(rb_cJS_Object, "__import_from_js", _rb_js_import_from_js, 0);
rb_define_method(rb_cJS_Object, "inspect", _rb_js_obj_inspect, 0);
rb_define_singleton_method(rb_cJS_Object, "wrap", _rb_js_obj_wrap, 1);

rb_define_method(rb_cInteger, "to_js", _rb_js_integer_to_js, 0);
rb_define_method(rb_cString, "to_js", _rb_js_string_to_js, 0);
Expand Down
139 changes: 101 additions & 38 deletions packages/npm-packages/ruby-head-wasm-wasi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import { DefaultRubyVM } from "ruby-head-wasm-wasi/dist/node.cjs.js";

const main = async () => {
const binary = await fs.readFile(
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
// (only nightly release for now)
// "./node_modules/ruby-head-wasm-wasi/dist/ruby.debug.wasm"
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
// (only nightly release for now)
// "./node_modules/ruby-head-wasm-wasi/dist/ruby.debug.wasm"
"./node_modules/ruby-head-wasm-wasi/dist/ruby.wasm"
);
const module = await WebAssembly.compile(binary);
Expand Down Expand Up @@ -62,9 +62,9 @@ See [the example project](https://github.com/ruby/ruby.wasm/tree/main/packages/n
const main = async () => {
// Fetch and instntiate WebAssembly binary
const response = await fetch(
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
// (only nightly release for now)
// "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.debug.wasm"
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
// (only nightly release for now)
// "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.debug.wasm"
"https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.wasm"
);
const buffer = await response.arrayBuffer();
Expand All @@ -85,44 +85,68 @@ See [the example project](https://github.com/ruby/ruby.wasm/tree/main/packages/n
</html>
```

<!-- The APIs section was generated by `npx documentation readme ./dist/index.esm.js --section=APIs` -->
## GC limitation with JavaScript interoperability

Since JavaScript's GC system and Ruby's GC system are separated and not cooperative, they cannot collect cyclic references between JavaScript and Ruby objects.

The following code will cause a memory leak:

```javascript
class JNode {
setRNode(rnode) {
this.rnode = rnode;
}
}
jnode = new JNode();

rnode = vm.eval(`
class RNode
def set_jnode(jnode)
@jnode = jnode
end
end
RNode.new
`);

rnode.call("set_jnode", vm.wrap(jnode));
jnode.setRNode(rnode);
```

<!-- The APIs section was generated by `npx documentation readme ../ruby-wasm-wasi/dist/index.esm.js --section=APIs` -->

## APIs

<!-- Generated by documentation.js. Update this documentation by updating the source code. -->

#### Table of Contents

- [ruby-head-wasm-wasi](#ruby-head-wasm-wasi)
- [Installation](#installation)
- [Quick Start (for Node.js)](#quick-start-for-nodejs)
- [Quick Start (for Browser)](#quick-start-for-browser)
- [APIs](#apis)
- [Table of Contents](#table-of-contents)
- [RubyVM](#rubyvm)
- [Examples](#examples)
- [initialize](#initialize)
- [Parameters](#parameters)
- [setInstance](#setinstance)
- [Parameters](#parameters-1)
- [addToImports](#addtoimports)
- [Parameters](#parameters-2)
- [printVersion](#printversion)
- [eval](#eval)
- [Parameters](#parameters-3)
- [Examples](#examples-1)
- [RbValue](#rbvalue)
- [Parameters](#parameters-4)
- [call](#call)
- [Parameters](#parameters-5)
- [Examples](#examples-2)
- [toPrimitive](#toprimitive)
- [Parameters](#parameters-6)
- [toString](#tostring)
- [toJS](#tojs)
- [RbError](#rberror)
- [Parameters](#parameters-7)
- [Building the package from source](#building-the-package-from-source)
- [RubyVM](#rubyvm)
- [Examples](#examples)
- [initialize](#initialize)
- [Parameters](#parameters)
- [setInstance](#setinstance)
- [Parameters](#parameters-1)
- [addToImports](#addtoimports)
- [Parameters](#parameters-2)
- [printVersion](#printversion)
- [eval](#eval)
- [Parameters](#parameters-3)
- [Examples](#examples-1)
- [wrap](#wrap)
- [Parameters](#parameters-4)
- [Examples](#examples-2)
- [JsValueTransport](#jsvaluetransport)
- [RbValue](#rbvalue)
- [Parameters](#parameters-5)
- [call](#call)
- [Parameters](#parameters-6)
- [Examples](#examples-3)
- [toPrimitive](#toprimitive)
- [Parameters](#parameters-7)
- [toString](#tostring)
- [toJS](#tojs)
- [RbError](#rberror)
- [Parameters](#parameters-8)

### RubyVM

Expand All @@ -142,6 +166,7 @@ vm.addToImports(imports);
const instance = await WebAssembly.instantiate(rubyModule, imports);
await vm.setInstance(instance);
wasi.initialize(instance);
vm.initialize();
```

#### initialize
Expand Down Expand Up @@ -196,6 +221,44 @@ console.log(result.toString()); // 3

Returns **any** the result of the last expression

#### wrap

Wrap a JavaScript value into a Ruby JS::Object

##### Parameters

- `value` The value to convert to RbValue

##### Examples

```javascript
const hash = vm.eval(`Hash.new`);
hash.call("store", vm.eval(`"key1"`), vm.wrap(new Object()));
```

Returns **any** the RbValue object representing the given JS value

### JsValueTransport

Export a JS value held by the Ruby VM to the JS environment.
This is implemented in a dirty way since wit cannot reference resources
defined in other interfaces.
In our case, we can't express `function(v: rb-abi-value) -> js-abi-value`
because `rb-js-abi-host.wit`, that defines `js-abi-value`, is implemented
by embedder side (JS) but `rb-abi-guest.wit`, that defines `rb-abi-value`
is implemented by guest side (Wasm).

This class is a helper to export by:

1. Call `function __export_to_js(v: rb-abi-value)` defined in guest from embedder side.
2. Call `function takeJsValue(v: js-abi-value)` defined in embedder from guest side with
underlying JS value of given `rb-abi-value`.
3. Then `takeJsValue` implementation escapes the given JS value to the `_takenJsValues`
stored in embedder side.
4. Finally, embedder side can take `_takenJsValues`.

Note that `exportJsValue` is not reentrant.

### RbValue

A RbValue is an object that represents a value in Ruby
Expand All @@ -204,7 +267,7 @@ A RbValue is an object that represents a value in Ruby

- `inner`
- `vm`
- `exporter`
- `privateObject`

#### call

Expand Down
5 changes: 2 additions & 3 deletions packages/npm-packages/ruby-wasm-wasi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

This package is a template for each channel-specific package.

| Channel | Package |
| ------- | ------- |
| Channel | Package |
| ------- | ------------------------------------------------- |
| `head` | [`ruby-head-wasm-wasi`](./../ruby-head-wasm-wasi) |

Loading