Skip to content

Commit a726700

Browse files
Merge pull request #18 from ruby/katei/feat-object-to-js-value
Allow wrapping Ruby and JS object to each other
2 parents 6ef5a1c + d0db285 commit a726700

File tree

8 files changed

+233
-68
lines changed

8 files changed

+233
-68
lines changed

ext/js/bindgen/rb-js-abi-host.wit

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ global-this: function() -> js-abi-value
77
int-to-js-number: function(value: s32) -> js-abi-value
88
string-to-js-string: function(value: string) -> js-abi-value
99
bool-to-js-bool: function(value: bool) -> js-abi-value
10+
rb-object-to-js-rb-value: function(raw-rb-abi-value: u32) -> js-abi-value
1011

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

13-
take-js-value: function(value: js-abi-value)
14+
export-js-value-to-host: function(value: js-abi-value)
15+
import-js-value-from-host: function() -> js-abi-value
1416

1517
reflect-apply: function(target: js-abi-value, this-argument: js-abi-value, arguments: list<js-abi-value>) -> js-abi-value
1618
reflect-construct: function(target: js-abi-value, arguments: list<js-abi-value>) -> js-abi-value

ext/js/js-core.c

+21-4
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ static VALUE _rb_js_is_js(VALUE _, VALUE obj) {
102102
* Try to convert the given object to a JS::Object using <code>to_js</code>
103103
* method. Returns <code>nil</code> if the object cannot be converted.
104104
*
105-
* p JS.try_convert(1) # => 1
106-
* p JS.try_convert("foo") # => "foo"
107-
* p JS.try_convert(Object.new) # => nil
105+
* p JS.try_convert(1) # => JS::Object
106+
* p JS.try_convert("foo") # => JS::Object
107+
* p JS.try_convert(Object.new) # => nil
108+
* p JS.try_convert(JS::Object.wrap(Object.new)) # => JS::Object
108109
*/
109110
VALUE _rb_js_try_convert(VALUE klass, VALUE obj) {
110111
if (_rb_js_is_js(klass, obj)) {
@@ -238,10 +239,24 @@ static VALUE _rb_js_obj_inspect(VALUE obj) {
238239
*/
239240
static VALUE _rb_js_export_to_js(VALUE obj) {
240241
struct jsvalue *p = check_jsvalue(obj);
241-
rb_js_abi_host_take_js_value(p->abi);
242+
rb_js_abi_host_export_js_value_to_host(p->abi);
242243
return Qnil;
243244
}
244245

246+
static VALUE _rb_js_import_from_js(VALUE obj) {
247+
return jsvalue_s_new(rb_js_abi_host_import_js_value_from_host());
248+
}
249+
250+
/*
251+
* call-seq:
252+
* JS::Object.wrap(obj) -> JS::Object
253+
*
254+
* Returns +obj+ wrapped by JS class RbValue.
255+
*/
256+
static VALUE _rb_js_obj_wrap(VALUE obj, VALUE wrapping) {
257+
return jsvalue_s_new(rb_js_abi_host_rb_object_to_js_rb_value((uint32_t)wrapping));
258+
}
259+
245260
/*
246261
* call-seq:
247262
* to_js -> JS::Object
@@ -305,7 +320,9 @@ void Init_js() {
305320
rb_define_method(rb_cJS_Object, "[]=", _rb_js_obj_aset, 2);
306321
rb_define_method(rb_cJS_Object, "call", _rb_js_obj_call, -1);
307322
rb_define_method(rb_cJS_Object, "__export_to_js", _rb_js_export_to_js, 0);
323+
rb_define_singleton_method(rb_cJS_Object, "__import_from_js", _rb_js_import_from_js, 0);
308324
rb_define_method(rb_cJS_Object, "inspect", _rb_js_obj_inspect, 0);
325+
rb_define_singleton_method(rb_cJS_Object, "wrap", _rb_js_obj_wrap, 1);
309326

310327
rb_define_method(rb_cInteger, "to_js", _rb_js_integer_to_js, 0);
311328
rb_define_method(rb_cString, "to_js", _rb_js_string_to_js, 0);

packages/npm-packages/ruby-head-wasm-wasi/README.md

+101-38
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ import { DefaultRubyVM } from "ruby-head-wasm-wasi/dist/node.cjs.js";
2626

2727
const main = async () => {
2828
const binary = await fs.readFile(
29-
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
30-
// (only nightly release for now)
31-
// "./node_modules/ruby-head-wasm-wasi/dist/ruby.debug.wasm"
29+
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
30+
// (only nightly release for now)
31+
// "./node_modules/ruby-head-wasm-wasi/dist/ruby.debug.wasm"
3232
"./node_modules/ruby-head-wasm-wasi/dist/ruby.wasm"
3333
);
3434
const module = await WebAssembly.compile(binary);
@@ -62,9 +62,9 @@ See [the example project](https://github.com/ruby/ruby.wasm/tree/main/packages/n
6262
const main = async () => {
6363
// Fetch and instntiate WebAssembly binary
6464
const response = await fetch(
65-
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
66-
// (only nightly release for now)
67-
// "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.debug.wasm"
65+
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
66+
// (only nightly release for now)
67+
// "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.debug.wasm"
6868
"https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.wasm"
6969
);
7070
const buffer = await response.arrayBuffer();
@@ -85,44 +85,68 @@ See [the example project](https://github.com/ruby/ruby.wasm/tree/main/packages/n
8585
</html>
8686
```
8787

88-
<!-- The APIs section was generated by `npx documentation readme ./dist/index.esm.js --section=APIs` -->
88+
## GC limitation with JavaScript interoperability
89+
90+
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.
91+
92+
The following code will cause a memory leak:
93+
94+
```javascript
95+
class JNode {
96+
setRNode(rnode) {
97+
this.rnode = rnode;
98+
}
99+
}
100+
jnode = new JNode();
101+
102+
rnode = vm.eval(`
103+
class RNode
104+
def set_jnode(jnode)
105+
@jnode = jnode
106+
end
107+
end
108+
RNode.new
109+
`);
110+
111+
rnode.call("set_jnode", vm.wrap(jnode));
112+
jnode.setRNode(rnode);
113+
```
114+
115+
<!-- The APIs section was generated by `npx documentation readme ../ruby-wasm-wasi/dist/index.esm.js --section=APIs` -->
89116

90117
## APIs
91118

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

94121
#### Table of Contents
95122

96-
- [ruby-head-wasm-wasi](#ruby-head-wasm-wasi)
97-
- [Installation](#installation)
98-
- [Quick Start (for Node.js)](#quick-start-for-nodejs)
99-
- [Quick Start (for Browser)](#quick-start-for-browser)
100-
- [APIs](#apis)
101-
- [Table of Contents](#table-of-contents)
102-
- [RubyVM](#rubyvm)
103-
- [Examples](#examples)
104-
- [initialize](#initialize)
105-
- [Parameters](#parameters)
106-
- [setInstance](#setinstance)
107-
- [Parameters](#parameters-1)
108-
- [addToImports](#addtoimports)
109-
- [Parameters](#parameters-2)
110-
- [printVersion](#printversion)
111-
- [eval](#eval)
112-
- [Parameters](#parameters-3)
113-
- [Examples](#examples-1)
114-
- [RbValue](#rbvalue)
115-
- [Parameters](#parameters-4)
116-
- [call](#call)
117-
- [Parameters](#parameters-5)
118-
- [Examples](#examples-2)
119-
- [toPrimitive](#toprimitive)
120-
- [Parameters](#parameters-6)
121-
- [toString](#tostring)
122-
- [toJS](#tojs)
123-
- [RbError](#rberror)
124-
- [Parameters](#parameters-7)
125-
- [Building the package from source](#building-the-package-from-source)
123+
- [RubyVM](#rubyvm)
124+
- [Examples](#examples)
125+
- [initialize](#initialize)
126+
- [Parameters](#parameters)
127+
- [setInstance](#setinstance)
128+
- [Parameters](#parameters-1)
129+
- [addToImports](#addtoimports)
130+
- [Parameters](#parameters-2)
131+
- [printVersion](#printversion)
132+
- [eval](#eval)
133+
- [Parameters](#parameters-3)
134+
- [Examples](#examples-1)
135+
- [wrap](#wrap)
136+
- [Parameters](#parameters-4)
137+
- [Examples](#examples-2)
138+
- [JsValueTransport](#jsvaluetransport)
139+
- [RbValue](#rbvalue)
140+
- [Parameters](#parameters-5)
141+
- [call](#call)
142+
- [Parameters](#parameters-6)
143+
- [Examples](#examples-3)
144+
- [toPrimitive](#toprimitive)
145+
- [Parameters](#parameters-7)
146+
- [toString](#tostring)
147+
- [toJS](#tojs)
148+
- [RbError](#rberror)
149+
- [Parameters](#parameters-8)
126150

127151
### RubyVM
128152

@@ -142,6 +166,7 @@ vm.addToImports(imports);
142166
const instance = await WebAssembly.instantiate(rubyModule, imports);
143167
await vm.setInstance(instance);
144168
wasi.initialize(instance);
169+
vm.initialize();
145170
```
146171

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

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

224+
#### wrap
225+
226+
Wrap a JavaScript value into a Ruby JS::Object
227+
228+
##### Parameters
229+
230+
- `value` The value to convert to RbValue
231+
232+
##### Examples
233+
234+
```javascript
235+
const hash = vm.eval(`Hash.new`);
236+
hash.call("store", vm.eval(`"key1"`), vm.wrap(new Object()));
237+
```
238+
239+
Returns **any** the RbValue object representing the given JS value
240+
241+
### JsValueTransport
242+
243+
Export a JS value held by the Ruby VM to the JS environment.
244+
This is implemented in a dirty way since wit cannot reference resources
245+
defined in other interfaces.
246+
In our case, we can't express `function(v: rb-abi-value) -> js-abi-value`
247+
because `rb-js-abi-host.wit`, that defines `js-abi-value`, is implemented
248+
by embedder side (JS) but `rb-abi-guest.wit`, that defines `rb-abi-value`
249+
is implemented by guest side (Wasm).
250+
251+
This class is a helper to export by:
252+
253+
1. Call `function __export_to_js(v: rb-abi-value)` defined in guest from embedder side.
254+
2. Call `function takeJsValue(v: js-abi-value)` defined in embedder from guest side with
255+
underlying JS value of given `rb-abi-value`.
256+
3. Then `takeJsValue` implementation escapes the given JS value to the `_takenJsValues`
257+
stored in embedder side.
258+
4. Finally, embedder side can take `_takenJsValues`.
259+
260+
Note that `exportJsValue` is not reentrant.
261+
199262
### RbValue
200263

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

205268
- `inner`
206269
- `vm`
207-
- `exporter`
270+
- `privateObject`
208271

209272
#### call
210273

packages/npm-packages/ruby-wasm-wasi/README.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

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

5-
| Channel | Package |
6-
| ------- | ------- |
5+
| Channel | Package |
6+
| ------- | ------------------------------------------------- |
77
| `head` | [`ruby-head-wasm-wasi`](./../ruby-head-wasm-wasi) |
8-

0 commit comments

Comments
 (0)