jscall 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +186 -25
- data/lib/jscall/browser.mjs +36 -11
- data/lib/jscall/browser.rb +29 -7
- data/lib/jscall/main.mjs +186 -54
- data/lib/jscall/version.rb +1 -1
- data/lib/jscall.rb +244 -74
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5fb1072aa12a5e884d9c92537f6ae7a5c632d4940715008865ec9760940f1c2
|
4
|
+
data.tar.gz: 597c3199f5ee21f98d80a42b05bf9822b729a22311d7c9f43e2cf45d54594983
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54c16c0015f2296309523ecf5d966bbf2071303136afef04e0bbdf139aa204c4b3db332a73ea65c0194fce7cea795503db17577805b4110a56edda796fca0a0b
|
7
|
+
data.tar.gz: ada3a68d6c19093c22087982e520ed18a0f3f3709584fc2c30978aed7909c1b6b8568eb48e24fde6c57cc759dc235b1a824524b1c0d43053d4d80512767339a8
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Jscall
|
2
2
|
|
3
|
+
[![Ruby](https://github.com/csg-tokyo/jscall/actions/workflows/ruby.yml/badge.svg)](https://github.com/csg-tokyo/jscall/actions/workflows/ruby.yml)
|
4
|
+
|
3
5
|
Jscall allows executing a program in JavaScript on node.js or a web browser.
|
4
6
|
By default, node.js is used for the execution.
|
5
7
|
To choose a web browser, call `Jscall.config`.
|
@@ -19,10 +21,14 @@ This returns `2`. The argument passed to `Jscall.exec` can be
|
|
19
21
|
multipe lines. It is executed as source code written in JavaScript.
|
20
22
|
|
21
23
|
`Jscall.exec` returns a resulting value. Numbers, character strings (and symbols), boolean values, and `nil` (and `null`)
|
22
|
-
are copied when passing between Ruby and JavaScript. An array is
|
24
|
+
are copied when passing between Ruby and JavaScript. An array is shallow-copied.
|
23
25
|
Other objects are not copied. When they are passed, a remote reference is created at the destination.
|
26
|
+
When a `Map` object is returned from JavaScript to Ruby, it is also
|
27
|
+
shallow-copied but into a `Hash` object in Ruby.
|
24
28
|
|
25
|
-
A remote reference is a
|
29
|
+
A remote reference is a local reference to a proxy object.
|
30
|
+
A method call on a remote reference invokes a method on the corresponding
|
31
|
+
object on the remote site. For example,
|
26
32
|
|
27
33
|
```
|
28
34
|
js_obj = Jscall.exec '({ foo: (x) => x + 1, bar: 7 })'
|
@@ -35,10 +41,29 @@ js_obj.baz # 9
|
|
35
41
|
The `foo` method is executed in JavaScript.
|
36
42
|
Since `bar` is not a function, its value is returned to Ruby as it is.
|
37
43
|
|
38
|
-
Setting
|
44
|
+
Setting an object property to a given value is also
|
39
45
|
allowed. The expression `js_obj.baz = 9` above sets
|
40
46
|
the object property `baz` to 9.
|
41
47
|
|
48
|
+
An argument to a JavaScript method is copied from Ruby to
|
49
|
+
JavaScript unless it is an object. When an argument is a Ruby object,
|
50
|
+
a proxy object is created in JavaScript. The rule is the same as the
|
51
|
+
rule for returning a value from JavaScript to Ruby. A primitive
|
52
|
+
value is copied but an object is not. An array is shallow-copied.
|
53
|
+
|
54
|
+
A `Hash` object in Ruby is also shallow-copied into JavaScript but a normal
|
55
|
+
object is created in JavaScript. Recall that a JavaScript object is
|
56
|
+
regarded as an associative array, or a hash table as in Ruby.
|
57
|
+
For example, in Ruby,
|
58
|
+
|
59
|
+
```
|
60
|
+
obj = { a: 2, b: 3 }
|
61
|
+
```
|
62
|
+
|
63
|
+
when this ruby object is passed to JavaScript as an argument,
|
64
|
+
a normal object `{ a: 2, b: 3 }` is created as its copy in JavaScript
|
65
|
+
and passed to a JavaScript method.
|
66
|
+
|
42
67
|
To call a JavaScript function from Ruby, call a mehtod on `Jscall`.
|
43
68
|
For example,
|
44
69
|
|
@@ -54,9 +79,22 @@ Jscall.foo(7) # 8
|
|
54
79
|
`Jscall.foo(7)` invokes the JavaScript function with the name following `Jscall.`
|
55
80
|
with the given argument. In this case,
|
56
81
|
the `foo` function is executed with the argument `7`.
|
82
|
+
Arguments and a return value are passed to/from a function
|
83
|
+
as they are passed to/from a method.
|
84
|
+
|
85
|
+
`Jscall` can be used for obtaining a remote reference to access a global variable
|
86
|
+
in JavaScript. For example,
|
87
|
+
|
88
|
+
```
|
89
|
+
Jscall.console.log('Hello')
|
90
|
+
```
|
91
|
+
|
92
|
+
This prints `Hello` on a JavaScript console. `Jscall.console` returns a remote
|
93
|
+
refererence to the value of `console` in JavaScript. Then, `.log('Hello')`
|
94
|
+
calls the `log` method on `console` in JavaScript.
|
57
95
|
|
58
96
|
When a Ruby object is passed to a JavaScript function/method,
|
59
|
-
|
97
|
+
you can call a method on the passed Ruby object.
|
60
98
|
|
61
99
|
```
|
62
100
|
Jscall.exec <<CODE
|
@@ -84,10 +122,19 @@ CODE
|
|
84
122
|
Jscall.foo()
|
85
123
|
```
|
86
124
|
|
87
|
-
`Jscall.foo()` returns the result of evaluating
|
88
|
-
in Ruby.
|
125
|
+
`Jscall.foo()` returns the result of evaluating given Ruby code
|
126
|
+
`RUBY_VERSION` in Ruby.
|
89
127
|
Don't forget to `await` a call to `Ruby.exec`.
|
90
128
|
|
129
|
+
### Remote references
|
130
|
+
|
131
|
+
A remote reference is implemented by a local reference to a proxy
|
132
|
+
object representing the remote object that the remote reference refers to.
|
133
|
+
When a proxy object is passed as an argument or a return value
|
134
|
+
from Ruby to JavaScript (or vice versa), the corresponding JavaScript
|
135
|
+
(or Ruby) object is passed to the destination. In other words,
|
136
|
+
a remote reference passed is converted back to a local reference.
|
137
|
+
|
91
138
|
Remote references will be automatically reclaimed when they are no
|
92
139
|
longer used. To reclaim them immediately, call:
|
93
140
|
|
@@ -95,6 +142,25 @@ longer used. To reclaim them immediately, call:
|
|
95
142
|
Jscall.scavenge_references
|
96
143
|
```
|
97
144
|
|
145
|
+
As mentioned above, a remote reference is a local reference
|
146
|
+
to a proxy object. In Ruby,
|
147
|
+
even a proxy object provides a number of methods inherited from `Object` class,
|
148
|
+
such as `clone`, `to_s`, and `inspect`. A call to such a method is not
|
149
|
+
delegated to the corresponding JavaScript object. To invoke such a method
|
150
|
+
on a JavaScript object, call `send` on its proxy object.
|
151
|
+
For example,
|
152
|
+
|
153
|
+
```
|
154
|
+
js_obj = Jscall.exec '({ to_s: (x, y) => x + y })'
|
155
|
+
puts js_obj.to_s(3, 4) # error
|
156
|
+
puts js_obj.send('to_s', 3, 4) # 7
|
157
|
+
```
|
158
|
+
|
159
|
+
The `send` method invokes the JavaScript method with the name specified
|
160
|
+
by the first argument. The remaining arguments passed to `send` are passed
|
161
|
+
to that JavaScript method.
|
162
|
+
|
163
|
+
|
98
164
|
## DOM manipulation
|
99
165
|
|
100
166
|
When JavaScript code is run on a browser, some utility methods
|
@@ -109,7 +175,16 @@ links `mystyle.css` in the current directory.
|
|
109
175
|
- `Jscall.dom.print(msg)`
|
110
176
|
|
111
177
|
This adds a `p` element to the DOM tree.
|
178
|
+
Its inner text is the character string passed as `msg`.
|
112
179
|
|
180
|
+
- `Jscall.dom.append_to_body(html_source)`
|
181
|
+
|
182
|
+
This inserts the given `html_source` at the end of the `body` element.
|
183
|
+
It is a shorthand for
|
184
|
+
|
185
|
+
```
|
186
|
+
Jscall.document.body.insertAdjacentHTML('beforeend', html_source)
|
187
|
+
```
|
113
188
|
|
114
189
|
## Variable scope
|
115
190
|
|
@@ -142,16 +217,33 @@ for loading a CommonJS module. For example,
|
|
142
217
|
Jscall.exec "mime = require('./mime.js')"
|
143
218
|
```
|
144
219
|
|
145
|
-
The file `./mime.js` is loaded and the module is bound to a global variable `mime
|
220
|
+
The file `./mime.js` is loaded and the module is bound to a global variable `mime` in JavaScript.
|
221
|
+
|
222
|
+
You can directly call `require` on `Jscall` in Ruby.
|
223
|
+
|
224
|
+
```
|
225
|
+
parser = Jscall.require("@babel/parser")
|
226
|
+
ast = parser.parse('const i = 3')
|
227
|
+
Jscall.console.log(ast)
|
228
|
+
```
|
229
|
+
|
230
|
+
`require` will search `./node_modules/` for `@babel/parser`.
|
231
|
+
This is equivalent to the following JavaScript code.
|
146
232
|
|
147
|
-
|
233
|
+
```
|
234
|
+
parser = require("@babel/parser")
|
235
|
+
ast = parser.parse('const i = 3')
|
236
|
+
console.log(ast)
|
237
|
+
```
|
238
|
+
|
239
|
+
Dynamic importing is also available. Call `Jscall.dyn_import` in Ruby.
|
148
240
|
|
149
241
|
```
|
150
242
|
fs = Jscall.dyn_import('fs')
|
151
243
|
```
|
152
244
|
|
153
245
|
This executes dynamic importing in JavaScript.
|
154
|
-
For node.js, the file name of the imported module should be a full path name. For a web browser, the root directory is
|
246
|
+
For node.js, the file name of the imported module should be a full path name. For a web browser, the root directory is the current working directory. So `Jscall.dyn_import('/mine.mjs')` loads the file `./mine.mjs`.
|
155
247
|
|
156
248
|
`Jscall.dyn_import` takes the second argument. If it is given,
|
157
249
|
a global variable in JavaScript is bound to the loaded module.
|
@@ -166,49 +258,112 @@ This is quite equivalent to the following JavaScript code:
|
|
166
258
|
fs_module = await load('fs')
|
167
259
|
```
|
168
260
|
|
261
|
+
## Promise
|
262
|
+
|
263
|
+
If a program attempts to pass a `Promise` object from JavaScript to Ruby,
|
264
|
+
it waits until the promise is fullfilled. Then Jscall passes
|
265
|
+
the value of that promise from JavaScript to Ruby instead of that
|
266
|
+
promise itself (or a remote reference to that promise). When that promise
|
267
|
+
is rejected, an error object is passed to Ruby
|
268
|
+
so that the error will be raised in Ruby.
|
269
|
+
This design reflects the fact that an `async` function in JavaScript
|
270
|
+
also returns a `Promise` object but this object must not be returned
|
271
|
+
to Ruby as is when that `async` function is called from Ruby.
|
272
|
+
Jscall cannot determine whether a promise should be passed as is to Ruby
|
273
|
+
or its value must be passed to Ruby after the promise is fullfilled.
|
274
|
+
|
275
|
+
When enforcing Jscall to pass a `Promise` object from JavaScript to Ruby,
|
276
|
+
`.async` must be inserted between a receiver and a method name.
|
277
|
+
|
278
|
+
```
|
279
|
+
Jscall.exec(<<CODE)
|
280
|
+
function make_promise() {
|
281
|
+
return { a: Promise.resolve(7) }
|
282
|
+
}
|
283
|
+
CODE
|
284
|
+
|
285
|
+
obj = Jscall.make_promise
|
286
|
+
result = obj.a # 7
|
287
|
+
prom = obj.async.a # promise
|
288
|
+
prom.then(->(r) { puts r }) # 7
|
289
|
+
```
|
290
|
+
|
169
291
|
|
170
292
|
## Configuration
|
171
293
|
|
172
|
-
|
294
|
+
Jscall supports several configuration options.
|
295
|
+
Call `Jscall.config` with necessary options.
|
296
|
+
|
297
|
+
### module_names:
|
298
|
+
|
299
|
+
To import JavaScript modules when node.js or a web browser starts,
|
173
300
|
|
174
301
|
```
|
175
|
-
Jscall.config(module_names: [["Foo", "./foo.mjs"], ["Bar", "./bar.mjs"]]
|
302
|
+
Jscall.config(module_names: [["Foo", "./js", "/lib/foo.mjs"], ["Bar", "./js", "/lib/bar.mjs"]])
|
176
303
|
```
|
177
304
|
|
178
|
-
This specifies that `./foo.mjs` and `./bar.mjs` are
|
305
|
+
This specifies that `./js/lib/foo.mjs` and `./js/lib/bar.mjs` are imported
|
306
|
+
at the beginning.
|
179
307
|
This is equivalent to the following import declarations:
|
180
308
|
|
181
309
|
```
|
182
|
-
import * as "Foo" from "./foo.mjs"
|
183
|
-
import * as "Bar" from "./bar.mjs"
|
310
|
+
import * as "Foo" from "./js/lib/foo.mjs"
|
311
|
+
import * as "Bar" from "./js/lib/bar.mjs"
|
312
|
+
```
|
313
|
+
|
314
|
+
Note that each array element given to `module_names:` is
|
315
|
+
|
316
|
+
```
|
317
|
+
[<module_name> <root> <path>]
|
184
318
|
```
|
185
319
|
|
186
|
-
|
320
|
+
`<path>` must start with `/`. It is used as a part of the URL when a browser
|
321
|
+
accesses a module.
|
322
|
+
When importing a module for node.js, `<root>` and `<path>` are concatenated
|
323
|
+
to form a full path name.
|
187
324
|
|
188
|
-
|
325
|
+
`<path>` must not start with `/jscall` or `/cmd`. They are reserved for
|
326
|
+
internal use.
|
189
327
|
|
190
|
-
|
328
|
+
### options:
|
329
|
+
|
330
|
+
To specify a command line argument passed to node.js,
|
191
331
|
|
192
332
|
```
|
193
|
-
Jscall
|
333
|
+
Jscall.config(options: '--use-strict')
|
194
334
|
```
|
195
335
|
|
196
|
-
|
336
|
+
This call specifies that
|
337
|
+
`--use-strict` is passed as a command line argument.
|
338
|
+
|
339
|
+
### browser: and port:
|
197
340
|
|
198
341
|
When running JavaScript code on a web browser,
|
199
342
|
|
200
343
|
```
|
201
|
-
Jscall.config(browser: true,
|
344
|
+
Jscall.config(browser: true, port: 10082)
|
202
345
|
```
|
203
346
|
|
204
|
-
`
|
347
|
+
Passing `true` for `browser:` switches the execution engine to a web browser.
|
348
|
+
The default engine is node.js.
|
349
|
+
To switch the engine back to node.js, pass `false` for `browser:`.
|
350
|
+
Call `Jscall.close` to detach the current execution engine.
|
351
|
+
A new enigine with a new configuration will be created.
|
352
|
+
|
353
|
+
`port:` specifies the port number of an http server. It is optional.
|
205
354
|
The example above specifies that Ruby receives http requests
|
206
355
|
sent to http://localhost:10082 from JavaScript on a web browser.
|
207
356
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
357
|
+
|
358
|
+
### Other configurations
|
359
|
+
|
360
|
+
To change the name of the node command,
|
361
|
+
|
362
|
+
```
|
363
|
+
Jscall::PipeToJs.node_command = "node.exe"
|
364
|
+
```
|
365
|
+
|
366
|
+
The default command name is `"node"`.
|
212
367
|
|
213
368
|
To change the command for launching a web browser,
|
214
369
|
|
@@ -224,6 +379,12 @@ Jscall launches a web browser by the command like the following:
|
|
224
379
|
open http://localhost:10082/jscall/jscall.html
|
225
380
|
```
|
226
381
|
|
382
|
+
Jscall generates a verbose error message if its debug level is more than 0.
|
383
|
+
|
384
|
+
```
|
385
|
+
Jscall.debug = 1
|
386
|
+
```
|
387
|
+
|
227
388
|
## Installation
|
228
389
|
|
229
390
|
Add this line to your application's Gemfile:
|
data/lib/jscall/browser.mjs
CHANGED
@@ -2,24 +2,27 @@
|
|
2
2
|
|
3
3
|
export class HttpStream {
|
4
4
|
constructor() {
|
5
|
-
this.
|
5
|
+
this.recv_buffer = []
|
6
6
|
this.send_callback = null
|
7
|
+
this.fetching = null
|
8
|
+
this.call_queue = []
|
9
|
+
this.puts('start')
|
7
10
|
}
|
8
11
|
|
9
12
|
[Symbol.asyncIterator]() {
|
10
13
|
const http_stream = this
|
11
14
|
return {
|
12
15
|
next() {
|
13
|
-
let
|
14
|
-
if (
|
16
|
+
let next_data = http_stream.recv_buffer.shift()
|
17
|
+
if (next_data === undefined)
|
15
18
|
return new Promise((resolve, reject) => {
|
16
19
|
if (http_stream.send_callback === null)
|
17
20
|
http_stream.send_callback = resolve
|
18
21
|
else
|
19
22
|
throw new Error('(fatal) send_callback is not null!')
|
20
|
-
})
|
23
|
+
})
|
21
24
|
else
|
22
|
-
return
|
25
|
+
return next_data
|
23
26
|
}
|
24
27
|
}
|
25
28
|
}
|
@@ -40,12 +43,30 @@ export class HttpStream {
|
|
40
43
|
}
|
41
44
|
|
42
45
|
puts(msg) {
|
43
|
-
this.
|
44
|
-
|
45
|
-
|
46
|
-
this.
|
47
|
-
|
48
|
-
|
46
|
+
if (this.fetching === null)
|
47
|
+
this.fetching = this.puts0(msg)
|
48
|
+
else
|
49
|
+
this.call_queue.push(msg)
|
50
|
+
}
|
51
|
+
|
52
|
+
puts0(msg) {
|
53
|
+
return this.do_fetch(msg)
|
54
|
+
.then((data) => {
|
55
|
+
if (this.send_callback === null)
|
56
|
+
this.recv_buffer.push(data)
|
57
|
+
else {
|
58
|
+
const callback = this.send_callback
|
59
|
+
this.send_callback = null
|
60
|
+
callback(data)
|
61
|
+
}
|
62
|
+
|
63
|
+
if (this.call_queue.length > 0) {
|
64
|
+
const msg2 = this.call_queue.shift()
|
65
|
+
this.fetching = this.puts0(msg2)
|
66
|
+
}
|
67
|
+
else
|
68
|
+
this.fetching = null
|
69
|
+
})
|
49
70
|
}
|
50
71
|
|
51
72
|
setEncoding(encoding) {}
|
@@ -64,4 +85,8 @@ export const Jscall = new class {
|
|
64
85
|
link.href = file_name
|
65
86
|
document.head.append(link)
|
66
87
|
}
|
88
|
+
|
89
|
+
append_to_body(html_source) {
|
90
|
+
document.body.insertAdjacentHTML('beforeend', html_source)
|
91
|
+
}
|
67
92
|
}
|
data/lib/jscall/browser.rb
CHANGED
@@ -8,6 +8,7 @@ module Jscall
|
|
8
8
|
WEBrick::HTTPUtils::DefaultMimeTypes['mjs'] ||= "application/javascript"
|
9
9
|
|
10
10
|
class Dom
|
11
|
+
# see Jscall class in browser.mjs
|
11
12
|
def method_missing(name, *args)
|
12
13
|
Jscall.__getpipe__.funcall(nil, "Jscall.#{name}", args)
|
13
14
|
end
|
@@ -23,13 +24,27 @@ module Jscall
|
|
23
24
|
end
|
24
25
|
|
25
26
|
class PipeToBrowser < PipeToJs
|
26
|
-
def startJS(module_names,
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
def startJS(module_names, config)
|
28
|
+
urls = {}
|
29
|
+
module_names.each_index do |i|
|
30
|
+
mod = module_names[i]
|
31
|
+
urls[mod[2]] = mod
|
32
|
+
end
|
33
|
+
port = config[:port] || 10081
|
34
|
+
@pipe = FetchServer.new(port, urls)
|
30
35
|
@pipe.open
|
31
36
|
end
|
32
37
|
|
38
|
+
def setup(config)
|
39
|
+
if config[:browser]
|
40
|
+
module_names = config[:module_names] || []
|
41
|
+
module_names.each_index do |i|
|
42
|
+
mod = module_names[i]
|
43
|
+
Jscall.dyn_import(mod[2], mod[0])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
33
48
|
def close
|
34
49
|
@pipe.shutdown
|
35
50
|
sleep(0.5)
|
@@ -58,7 +73,8 @@ module Jscall
|
|
58
73
|
@@run_cmd = name
|
59
74
|
end
|
60
75
|
|
61
|
-
def initialize(port)
|
76
|
+
def initialize(port, urls)
|
77
|
+
@url_map = urls
|
62
78
|
@send_buffer = Thread::Queue.new
|
63
79
|
@receive_buffer = Thread::Queue.new
|
64
80
|
@server_running = false
|
@@ -96,10 +112,16 @@ module Jscall
|
|
96
112
|
end
|
97
113
|
|
98
114
|
def read_file(req, res)
|
99
|
-
|
115
|
+
path = req.path
|
116
|
+
if path.start_with?('/jscall/')
|
100
117
|
root = "#{__dir__}/../"
|
101
118
|
else
|
102
|
-
|
119
|
+
value = @url_map[path]
|
120
|
+
if value.nil?
|
121
|
+
root = @server.config[:DocumentRoot]
|
122
|
+
else
|
123
|
+
root = value[1]
|
124
|
+
end
|
103
125
|
end
|
104
126
|
WEBrick::HTTPServlet::FileHandler.new(@server, root).service(req, res)
|
105
127
|
end
|
data/lib/jscall/main.mjs
CHANGED
@@ -3,10 +3,16 @@
|
|
3
3
|
const cmd_eval = 1
|
4
4
|
const cmd_call = 2
|
5
5
|
const cmd_reply = 3
|
6
|
+
const cmd_async_call = 4
|
7
|
+
const cmd_async_eval = 5
|
8
|
+
const cmd_retry = 6
|
9
|
+
const cmd_reject = 7
|
10
|
+
|
6
11
|
const param_array = 0
|
7
12
|
const param_object = 1
|
8
13
|
const param_local_object = 2
|
9
14
|
const param_error = 3
|
15
|
+
const param_hash = 4
|
10
16
|
|
11
17
|
const table_size = 100
|
12
18
|
|
@@ -64,8 +70,9 @@ const exported = new class {
|
|
64
70
|
}
|
65
71
|
}
|
66
72
|
|
67
|
-
|
73
|
+
class RemoteRef extends Function {
|
68
74
|
constructor(id) {
|
75
|
+
super()
|
69
76
|
this.id = id
|
70
77
|
}
|
71
78
|
}
|
@@ -74,12 +81,20 @@ const remoteRefHandler = new class {
|
|
74
81
|
get(obj, name) {
|
75
82
|
if (name === '__self__')
|
76
83
|
return obj
|
84
|
+
else if (name === 'then')
|
85
|
+
// to prevent the Promise from handling RemoteRefs as thenable
|
86
|
+
// e.g., `Jscall.exec("{ call: (x) => Promise.resolve(x) }").call(obj)' should return that obj itself
|
87
|
+
return undefined
|
77
88
|
else
|
78
89
|
return (...args) => {
|
79
90
|
// this returns Promise
|
80
91
|
return funcall_to_ruby(obj.id, name, args)
|
81
92
|
}
|
82
93
|
}
|
94
|
+
apply(obj, self, args) {
|
95
|
+
// this returns Promise
|
96
|
+
return funcall_to_ruby(obj.id, 'call', args)
|
97
|
+
}
|
83
98
|
}
|
84
99
|
|
85
100
|
const imported = new class {
|
@@ -94,7 +109,8 @@ const imported = new class {
|
|
94
109
|
if (obj !== null && obj !== undefined)
|
95
110
|
return obj
|
96
111
|
else {
|
97
|
-
const
|
112
|
+
const ref = new RemoteRef(index)
|
113
|
+
const rref = new Proxy(ref, remoteRefHandler)
|
98
114
|
this.objects[index] = new WeakRef(rref)
|
99
115
|
return rref
|
100
116
|
}
|
@@ -104,14 +120,15 @@ const imported = new class {
|
|
104
120
|
|
105
121
|
// collect reclaimed RemoteRef objects.
|
106
122
|
dead_references() {
|
107
|
-
|
123
|
+
// In Safari, deref() may return null
|
124
|
+
if (this.canary === null || this.canary.deref() == undefined)
|
108
125
|
this.canary = new WeakRef(new RemoteRef(-1))
|
109
126
|
else
|
110
127
|
return [] // the canary is alive, so no GC has happened yet.
|
111
128
|
|
112
129
|
const deads = []
|
113
130
|
this.objects.forEach((wref, index) => {
|
114
|
-
if (wref !== null && wref !== undefined && wref.deref()
|
131
|
+
if (wref !== null && wref !== undefined && wref.deref() == undefined) {
|
115
132
|
this.objects[index] = null
|
116
133
|
deads.push(index)
|
117
134
|
}
|
@@ -127,15 +144,21 @@ const encode_obj = obj => {
|
|
127
144
|
return null
|
128
145
|
else if (obj.constructor === Array)
|
129
146
|
return [param_array, obj.map(e => encode_obj(e))]
|
147
|
+
else if (obj instanceof Map) {
|
148
|
+
const hash = {}
|
149
|
+
for (const [key, value] of obj.entries())
|
150
|
+
hash[key] = value
|
151
|
+
return [param_hash, hash]
|
152
|
+
}
|
130
153
|
else if (obj instanceof RemoteRef)
|
131
154
|
return [param_local_object, exported.export_remoteref(obj)]
|
132
155
|
else
|
133
156
|
return [param_object, exported.export(obj)]
|
134
157
|
}
|
135
158
|
|
136
|
-
const encode_error = msg => [param_error, msg]
|
159
|
+
const encode_error = msg => [param_error, msg.toString()]
|
137
160
|
|
138
|
-
|
161
|
+
class RubyError {
|
139
162
|
constructor(msg) { this.message = msg }
|
140
163
|
get() { return 'RubyError: ' + this.message }
|
141
164
|
}
|
@@ -146,6 +169,12 @@ const decode_obj = obj => {
|
|
146
169
|
else if (obj.constructor === Array && obj.length == 2)
|
147
170
|
if (obj[0] == param_array)
|
148
171
|
return obj[1].map(e => decode_obj(e))
|
172
|
+
else if (obj[0] == param_hash) {
|
173
|
+
const hash = {}
|
174
|
+
for (const [key, value] of Object.entries(obj[1]))
|
175
|
+
hash[key] = decode_obj(value)
|
176
|
+
return hash
|
177
|
+
}
|
149
178
|
else if (obj[0] == param_object)
|
150
179
|
return imported.import(obj[1])
|
151
180
|
else if (obj[0] == param_local_object)
|
@@ -159,9 +188,9 @@ const decode_obj = obj => {
|
|
159
188
|
const js_eval = eval
|
160
189
|
|
161
190
|
const funcall_from_ruby = cmd => {
|
162
|
-
const receiver = decode_obj(cmd[
|
163
|
-
const name = cmd[
|
164
|
-
const args = cmd[
|
191
|
+
const receiver = decode_obj(cmd[2])
|
192
|
+
const name = cmd[3]
|
193
|
+
const args = cmd[4].map(e => decode_obj(e))
|
165
194
|
if (name.endsWith('=')) {
|
166
195
|
const name2 = name.substring(0, name.length - 1)
|
167
196
|
if (receiver === null)
|
@@ -189,34 +218,41 @@ const funcall_from_ruby = cmd => {
|
|
189
218
|
return f // obtain a propety
|
190
219
|
}
|
191
220
|
|
192
|
-
throw `unknown function/method was called
|
221
|
+
throw `unknown JS function/method was called: ${name} on <${receiver}>`
|
193
222
|
}
|
194
223
|
|
195
224
|
let stdout_puts = console.log
|
225
|
+
let num_generated_ids = 0
|
196
226
|
|
197
|
-
const
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
else {
|
202
|
-
if (last_callback_stack_depth < callback_stack.length)
|
203
|
-
throw 'Ruby code was called without await. Call Jscall.close for recovery'
|
227
|
+
const fresh_id = () => {
|
228
|
+
num_generated_ids += 1
|
229
|
+
return num_generated_ids
|
230
|
+
}
|
204
231
|
|
232
|
+
const reply = (message_id, value, sync_mode) => {
|
233
|
+
if (sync_mode && value instanceof Promise)
|
234
|
+
value.then(result => { reply(message_id, result, true) })
|
235
|
+
.catch(err => reply_error(message_id, err))
|
236
|
+
else {
|
205
237
|
try {
|
206
|
-
const cmd = reply_with_piggyback([cmd_reply, encode_obj(value)])
|
238
|
+
const cmd = reply_with_piggyback([cmd_reply, message_id, encode_obj(value)])
|
207
239
|
const data = JSON.stringify(cmd)
|
208
240
|
stdout_puts(data)
|
209
241
|
} catch (e) {
|
210
|
-
reply_error(e)
|
242
|
+
reply_error(message_id, e)
|
211
243
|
}
|
212
244
|
}
|
213
245
|
}
|
214
246
|
|
215
|
-
const reply_error = e => {
|
216
|
-
const cmd = reply_with_piggyback([cmd_reply, encode_error(e)])
|
247
|
+
const reply_error = (message_id, e) => {
|
248
|
+
const cmd = reply_with_piggyback([cmd_reply, message_id, encode_error(e)])
|
217
249
|
stdout_puts(JSON.stringify(cmd))
|
218
250
|
}
|
219
251
|
|
252
|
+
const puts_retry_cmd = msg_id => {
|
253
|
+
stdout_puts(JSON.stringify([cmd_retry, msg_id, encode_obj(false)]))
|
254
|
+
}
|
255
|
+
|
220
256
|
export const scavenge_references = () => {
|
221
257
|
reply_counter = 200
|
222
258
|
imported.kill_canary()
|
@@ -230,7 +266,7 @@ const reply_with_piggyback = cmd => {
|
|
230
266
|
const dead_refs = imported.dead_references()
|
231
267
|
if (dead_refs.length > 0) {
|
232
268
|
const cmd2 = cmd.concat()
|
233
|
-
cmd2[
|
269
|
+
cmd2[5] = dead_refs
|
234
270
|
return cmd2
|
235
271
|
}
|
236
272
|
}
|
@@ -239,55 +275,140 @@ const reply_with_piggyback = cmd => {
|
|
239
275
|
}
|
240
276
|
|
241
277
|
const callback_stack = []
|
242
|
-
let last_callback_stack_depth = 0
|
243
278
|
let reply_counter = 0
|
244
279
|
|
245
280
|
export const exec = src => {
|
246
281
|
return new Promise((resolve, reject) => {
|
247
|
-
const
|
248
|
-
|
282
|
+
const message_id = fresh_id()
|
283
|
+
const cmd = reply_with_piggyback([cmd_eval, message_id, src])
|
284
|
+
callback_stack.push([message_id, resolve, reject])
|
249
285
|
stdout_puts(JSON.stringify(cmd))
|
250
286
|
})
|
251
287
|
}
|
252
288
|
|
253
289
|
const funcall_to_ruby = (receiver_id, name, args) => {
|
254
290
|
return new Promise((resolve, reject) => {
|
291
|
+
const message_id = fresh_id()
|
255
292
|
const receiver = [param_local_object, receiver_id]
|
256
293
|
const encoded_args = args.map(e => encode_obj(e))
|
257
|
-
const cmd = reply_with_piggyback([cmd_call, receiver, name, encoded_args])
|
258
|
-
callback_stack.push([resolve, reject])
|
294
|
+
const cmd = reply_with_piggyback([cmd_call, message_id, receiver, name, encoded_args])
|
295
|
+
callback_stack.push([message_id, resolve, reject])
|
259
296
|
stdout_puts(JSON.stringify(cmd))
|
260
297
|
})
|
261
298
|
}
|
262
299
|
|
263
300
|
const returned_from_callback = cmd => {
|
264
|
-
const
|
265
|
-
const
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
301
|
+
const message_id = cmd[1]
|
302
|
+
const result = decode_obj(cmd[2])
|
303
|
+
for (let i = callback_stack.length - 1; i >= 0; i--) {
|
304
|
+
// check the most recent element first since callbacks are
|
305
|
+
// assumed to be synchronously executed
|
306
|
+
if (callback_stack[i][0] === message_id) {
|
307
|
+
const [[_, resolve, reject]] = callback_stack.splice(i, 1)
|
308
|
+
if (result instanceof RubyError)
|
309
|
+
reject(result.get())
|
310
|
+
else
|
311
|
+
resolve(result)
|
312
|
+
}
|
313
|
+
}
|
270
314
|
}
|
271
315
|
|
272
|
-
class
|
316
|
+
export class MessageReader {
|
317
|
+
static HeaderSize = 6
|
318
|
+
|
273
319
|
constructor(stream) {
|
274
|
-
this.stream
|
275
|
-
this.
|
320
|
+
this.stream = stream
|
321
|
+
this.state = "header"
|
322
|
+
this.acc = ""
|
323
|
+
this.bodySize = 0
|
324
|
+
}
|
325
|
+
|
326
|
+
parseHeader(pos) {
|
327
|
+
// skip leading whitespace characters as a countermeasure against leftover "\n"
|
328
|
+
while (pos < this.acc.length && /\s/.test(this.acc[pos]))
|
329
|
+
pos++
|
330
|
+
|
331
|
+
if (this.acc.length >= MessageReader.HeaderSize) {
|
332
|
+
const start = pos
|
333
|
+
pos += MessageReader.HeaderSize
|
334
|
+
return [parseInt(this.acc.slice(start, pos), 16), pos]
|
335
|
+
}
|
336
|
+
else
|
337
|
+
return undefined
|
338
|
+
}
|
339
|
+
|
340
|
+
parseBody(pos) {
|
341
|
+
if (this.acc.length >= this.bodySize) {
|
342
|
+
const start = pos
|
343
|
+
pos += this.bodySize
|
344
|
+
return [this.acc.slice(start, pos), pos]
|
345
|
+
}
|
346
|
+
else
|
347
|
+
return undefined
|
348
|
+
}
|
349
|
+
|
350
|
+
consume(pos) {
|
351
|
+
if (pos > 0)
|
352
|
+
this.acc = this.acc.slice(pos)
|
276
353
|
}
|
277
354
|
|
278
355
|
async *[Symbol.asyncIterator]() {
|
279
|
-
for await (
|
356
|
+
for await (const data of this.stream) {
|
280
357
|
this.acc += data
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
358
|
+
let pos = 0
|
359
|
+
while (true) {
|
360
|
+
const result = this.iteratorBody(pos)
|
361
|
+
if (result[0] === false)
|
362
|
+
break
|
363
|
+
else if (result[0] !== true)
|
364
|
+
yield result[0] // result[0] is a string
|
365
|
+
|
366
|
+
pos = result[1]
|
367
|
+
if (this.checkEmptiness(pos))
|
368
|
+
break
|
369
|
+
}
|
370
|
+
}
|
371
|
+
this.checkEOS()
|
372
|
+
}
|
373
|
+
|
374
|
+
iteratorBody(pos) {
|
375
|
+
if (this.state === "header") {
|
376
|
+
const header = this.parseHeader(pos)
|
377
|
+
if (header === undefined) {
|
378
|
+
this.consume(pos)
|
379
|
+
return [false, pos]
|
380
|
+
} else {
|
381
|
+
this.bodySize = header[0]
|
382
|
+
pos = header[1]
|
383
|
+
this.state = "body"
|
384
|
+
}
|
385
|
+
}
|
386
|
+
if (this.state === "body") {
|
387
|
+
const body = this.parseBody(pos)
|
388
|
+
if (body === undefined) {
|
389
|
+
this.consume(pos)
|
390
|
+
return [false, pos]
|
391
|
+
} else {
|
392
|
+
this.state = "header"
|
393
|
+
return body
|
286
394
|
}
|
287
395
|
}
|
288
|
-
|
289
|
-
|
396
|
+
return [true, pos]
|
397
|
+
}
|
398
|
+
|
399
|
+
checkEmptiness(pos) {
|
400
|
+
if (pos == this.acc.length || (pos == this.acc.length - 1
|
401
|
+
&& this.acc[this.acc.length - 1] === "\n")) {
|
402
|
+
this.acc = ""
|
403
|
+
return true
|
290
404
|
}
|
405
|
+
else
|
406
|
+
return false
|
407
|
+
}
|
408
|
+
|
409
|
+
checkEOS() {
|
410
|
+
if (this.acc.length > 0)
|
411
|
+
throw new Error("The pipe closed after receiving an incomplete message")
|
291
412
|
}
|
292
413
|
}
|
293
414
|
|
@@ -298,30 +419,41 @@ export const start = async (stdin, use_stdout) => {
|
|
298
419
|
stdout_puts = (m) => stdin.puts(m) // on browser
|
299
420
|
|
300
421
|
stdin.setEncoding('utf8')
|
301
|
-
for await (const
|
422
|
+
for await (const json_data of new MessageReader(stdin)) {
|
423
|
+
let cmd
|
302
424
|
try {
|
303
|
-
|
425
|
+
cmd = JSON.parse(json_data)
|
304
426
|
|
305
427
|
// scavenge remote references
|
306
|
-
if (cmd.length >
|
307
|
-
cmd[
|
428
|
+
if (cmd.length > 5)
|
429
|
+
cmd[5].forEach(i => exported.remove(i))
|
308
430
|
|
309
431
|
if (cmd[0] == cmd_eval) {
|
310
|
-
const result = js_eval(cmd[
|
311
|
-
reply(result)
|
432
|
+
const result = js_eval(cmd[2])
|
433
|
+
reply(cmd[1], result, true)
|
312
434
|
}
|
313
435
|
else if (cmd[0] == cmd_call) {
|
314
436
|
const result = funcall_from_ruby(cmd)
|
315
|
-
reply(result)
|
437
|
+
reply(cmd[1], result, true)
|
316
438
|
}
|
317
439
|
else if (cmd[0] == cmd_reply)
|
318
440
|
returned_from_callback(cmd)
|
319
|
-
else
|
320
|
-
|
441
|
+
else if (cmd[0] == cmd_async_call) {
|
442
|
+
const result = funcall_from_ruby(cmd)
|
443
|
+
reply(cmd[1], result, false)
|
444
|
+
}
|
445
|
+
else if (cmd[0] == cmd_async_eval) {
|
446
|
+
const result = js_eval(cmd[2])
|
447
|
+
reply(cmd[1], result, false)
|
448
|
+
}
|
449
|
+
else if (cmd[0] == cmd_reject)
|
450
|
+
puts_retry_cmd(cmd[1])
|
451
|
+
else // cmd_retry and other unknown commands
|
452
|
+
reply_error(cmd[1], `invalid command ${cmd[0]}`)
|
321
453
|
} catch (error) {
|
322
454
|
const msg = typeof error === 'string' ? error : error.toString() +
|
323
455
|
'\n ---\n' + error.stack
|
324
|
-
reply_error(msg)
|
456
|
+
reply_error(cmd[1], msg)
|
325
457
|
}
|
326
458
|
}
|
327
459
|
}
|
data/lib/jscall/version.rb
CHANGED
data/lib/jscall.rb
CHANGED
@@ -9,6 +9,16 @@ require_relative "jscall/version"
|
|
9
9
|
require_relative "jscall/browser"
|
10
10
|
|
11
11
|
module Jscall
|
12
|
+
@@debug = 0
|
13
|
+
|
14
|
+
# Current debug level (>= 0)
|
15
|
+
def self.debug() @@debug end
|
16
|
+
|
17
|
+
# Sets the current debug level.
|
18
|
+
def self.debug=(level)
|
19
|
+
@@debug = level
|
20
|
+
end
|
21
|
+
|
12
22
|
TableSize = 100
|
13
23
|
|
14
24
|
class JavaScriptError < RuntimeError
|
@@ -77,17 +87,50 @@ module Jscall
|
|
77
87
|
end
|
78
88
|
|
79
89
|
class RemoteRef
|
80
|
-
attr_reader :id
|
81
|
-
|
82
90
|
def initialize(id)
|
83
91
|
@id = id
|
84
92
|
end
|
85
93
|
|
94
|
+
def __get_id
|
95
|
+
@id
|
96
|
+
end
|
97
|
+
|
98
|
+
def async
|
99
|
+
AsyncRemoteRef.new(@id)
|
100
|
+
end
|
101
|
+
|
102
|
+
# override Object#then
|
103
|
+
def then(*args)
|
104
|
+
send('then', *args)
|
105
|
+
end
|
106
|
+
|
107
|
+
# puts() calls this
|
108
|
+
def to_ary
|
109
|
+
["#<RemoteRef @id=#{@id}>"]
|
110
|
+
end
|
111
|
+
|
112
|
+
# override Object#send
|
113
|
+
def send(name, *args)
|
114
|
+
Jscall.__getpipe__.funcall(self, name, args)
|
115
|
+
end
|
116
|
+
|
117
|
+
def async_send(name, *args)
|
118
|
+
Jscall.__getpipe__.async_funcall(self, name, args)
|
119
|
+
end
|
120
|
+
|
86
121
|
def method_missing(name, *args)
|
87
122
|
Jscall.__getpipe__.funcall(self, name, args)
|
88
123
|
end
|
89
124
|
end
|
90
125
|
|
126
|
+
class AsyncRemoteRef < RemoteRef
|
127
|
+
alias send async_send
|
128
|
+
|
129
|
+
def method_missing(name, *args)
|
130
|
+
Jscall.__getpipe__.async_funcall(self, name, args)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
91
134
|
class Imported # outbound references from Ruby to Node
|
92
135
|
attr_reader :objects
|
93
136
|
|
@@ -137,10 +180,19 @@ module Jscall
|
|
137
180
|
CMD_EVAL = 1
|
138
181
|
CMD_CALL = 2
|
139
182
|
CMD_REPLY = 3
|
183
|
+
CMD_ASYNC_CALL = 4
|
184
|
+
CMD_ASYNC_EVAL = 5
|
185
|
+
CMD_RETRY = 6
|
186
|
+
CMD_REJECT = 7
|
187
|
+
|
140
188
|
Param_array = 0
|
141
189
|
Param_object = 1
|
142
190
|
Param_local_object = 2
|
143
191
|
Param_error = 3
|
192
|
+
Param_hash = 4
|
193
|
+
|
194
|
+
Header_size = 6
|
195
|
+
Header_format = '%%0%dx' % Header_size
|
144
196
|
|
145
197
|
@@node_cmd = 'node'
|
146
198
|
|
@@ -148,21 +200,35 @@ module Jscall
|
|
148
200
|
@@node_cmd = cmd
|
149
201
|
end
|
150
202
|
|
151
|
-
|
152
|
-
#
|
153
|
-
def initialize(module_names=[], options='')
|
154
|
-
startJS(module_names, options)
|
203
|
+
def initialize(config)
|
155
204
|
@exported = Exported.new
|
156
205
|
@imported = Imported.new
|
157
206
|
@send_counter = 0
|
207
|
+
@num_generated_ids = 0
|
208
|
+
@pending_replies = {}
|
209
|
+
module_names = config[:module_names] || []
|
210
|
+
startJS(module_names, config)
|
158
211
|
end
|
159
212
|
|
160
|
-
|
213
|
+
def setup(config)
|
214
|
+
# called just after executing new PipeToJs(config)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Config options.
|
218
|
+
#
|
219
|
+
# module_names: an array of [module_name, module_root, module_file_name]
|
220
|
+
# For example,
|
221
|
+
# [['Foo', '/home/jscall', '/lib/foo.mjs']]
|
222
|
+
# this does
|
223
|
+
# import * as Foo from "/home/jscall/lib/foo.mjs"
|
224
|
+
#
|
225
|
+
# options: options passed to node.js
|
161
226
|
#
|
162
|
-
def startJS(module_names,
|
227
|
+
def startJS(module_names, config)
|
228
|
+
options = config[:options] || ''
|
163
229
|
script2 = ''
|
164
230
|
module_names.each_index do |i|
|
165
|
-
script2 += "import * as m#{i + 2} from \"#{module_names[i][1]}\"; globalThis.#{module_names[i][0]} = m#{i + 2}; "
|
231
|
+
script2 += "import * as m#{i + 2} from \"#{module_names[i][1]}#{module_names[i][2]}\"; globalThis.#{module_names[i][0]} = m#{i + 2}; "
|
166
232
|
end
|
167
233
|
script2 += "import { createRequire } from \"node:module\"; globalThis.require = createRequire(\"file://#{Dir.pwd}/\");"
|
168
234
|
script = "'import * as m1 from \"#{__dir__}/jscall/main.mjs\"; globalThis.Ruby = m1; #{script2}; Ruby.start(process.stdin, true)'"
|
@@ -184,8 +250,12 @@ module Jscall
|
|
184
250
|
obj
|
185
251
|
elsif obj.is_a?(Array)
|
186
252
|
[Param_array, obj.map {|e| encode_obj(e)}]
|
253
|
+
elsif obj.is_a?(Hash)
|
254
|
+
hash2 = {}
|
255
|
+
obj.each {|key, value| hash2[key] = encode_obj(value) }
|
256
|
+
[Param_hash, hash2]
|
187
257
|
elsif obj.is_a?(RemoteRef)
|
188
|
-
[Param_local_object, obj.
|
258
|
+
[Param_local_object, obj.__get_id]
|
189
259
|
else
|
190
260
|
[Param_object, @exported.export(obj)]
|
191
261
|
end
|
@@ -199,11 +269,15 @@ module Jscall
|
|
199
269
|
if obj.is_a?(Numeric) || obj.is_a?(String) || obj == true || obj == false || obj == nil
|
200
270
|
obj
|
201
271
|
elsif obj.is_a?(Array) && obj.size == 2
|
202
|
-
if
|
272
|
+
if obj[0] == Param_array
|
203
273
|
obj[1].map {|e| decode_obj(e)}
|
204
|
-
elsif
|
274
|
+
elsif obj[0] == Param_hash
|
275
|
+
hash = {}
|
276
|
+
obj[1].each {|key, value| hash[key] = decode_obj(value)}
|
277
|
+
hash
|
278
|
+
elsif obj[0] == Param_object
|
205
279
|
@imported.import(obj[1])
|
206
|
-
elsif
|
280
|
+
elsif obj[0] == Param_local_object
|
207
281
|
@exported.find(obj[1])
|
208
282
|
else # if Param_error
|
209
283
|
JavaScriptError.new(obj[1])
|
@@ -213,20 +287,38 @@ module Jscall
|
|
213
287
|
end
|
214
288
|
end
|
215
289
|
|
290
|
+
def fresh_id
|
291
|
+
@num_generated_ids += 1
|
292
|
+
end
|
293
|
+
|
216
294
|
def funcall(receiver, name, args)
|
217
|
-
cmd = [CMD_CALL, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
|
295
|
+
cmd = [CMD_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
|
296
|
+
send_command(cmd)
|
297
|
+
end
|
298
|
+
|
299
|
+
def async_funcall(receiver, name, args)
|
300
|
+
cmd = [CMD_ASYNC_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
|
218
301
|
send_command(cmd)
|
219
302
|
end
|
220
303
|
|
221
304
|
def exec(src)
|
222
|
-
cmd = [CMD_EVAL, src]
|
305
|
+
cmd = [CMD_EVAL, nil, src]
|
306
|
+
send_command(cmd)
|
307
|
+
end
|
308
|
+
|
309
|
+
def async_exec(src)
|
310
|
+
cmd = [CMD_ASYNC_EVAL, nil, src]
|
223
311
|
send_command(cmd)
|
224
312
|
end
|
225
313
|
|
226
314
|
def encode_eval_error(e)
|
227
315
|
traces = e.backtrace
|
228
316
|
location = if traces.size > 0 then traces[0] else '' end
|
229
|
-
|
317
|
+
if Jscall.debug > 0
|
318
|
+
encode_error("\n#{e.full_message}")
|
319
|
+
else
|
320
|
+
encode_error(location + ' ' + e.to_s)
|
321
|
+
end
|
230
322
|
end
|
231
323
|
|
232
324
|
def scavenge
|
@@ -243,7 +335,7 @@ module Jscall
|
|
243
335
|
dead_refs = @imported.dead_references()
|
244
336
|
if (dead_refs.length > 0)
|
245
337
|
cmd2 = cmd.dup
|
246
|
-
cmd2[
|
338
|
+
cmd2[5] = dead_refs
|
247
339
|
return cmd2
|
248
340
|
end
|
249
341
|
end
|
@@ -251,55 +343,103 @@ module Jscall
|
|
251
343
|
end
|
252
344
|
|
253
345
|
def send_command(cmd)
|
346
|
+
message_id = (cmd[1] ||= fresh_id)
|
254
347
|
json_data = JSON.generate(send_with_piggyback(cmd))
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
if reply.length > 4
|
259
|
-
reply[4].each {|idx| @exported.remove(idx) }
|
348
|
+
header = (Header_format % json_data.length)
|
349
|
+
if header.length != Header_size
|
350
|
+
raise "message length limit exceeded"
|
260
351
|
end
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
elsif reply[0] == CMD_EVAL
|
271
|
-
begin
|
272
|
-
result = Object::TOPLEVEL_BINDING.eval(reply[1])
|
273
|
-
encoded = encode_obj(result)
|
274
|
-
rescue => e
|
275
|
-
encoded = encode_eval_error(e)
|
352
|
+
json_data_with_header = header + json_data
|
353
|
+
@pipe.puts(json_data_with_header)
|
354
|
+
|
355
|
+
while true
|
356
|
+
reply_data = @pipe.gets
|
357
|
+
reply = JSON.parse(reply_data || '[]')
|
358
|
+
if reply.length > 5
|
359
|
+
reply[5].each {|idx| @exported.remove(idx) }
|
360
|
+
reply[5] = nil
|
276
361
|
end
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
362
|
+
if @pipe.closed?
|
363
|
+
raise RuntimeError.new("connection closed: #{reply}")
|
364
|
+
elsif reply[0] == CMD_REPLY
|
365
|
+
result = decode_obj(reply[2])
|
366
|
+
if reply[1] != message_id
|
367
|
+
@pending_replies[reply[1]] = result
|
368
|
+
send_reply(reply[1], nil, false, CMD_REJECT)
|
369
|
+
elsif result.is_a?(JavaScriptError)
|
370
|
+
raise result
|
371
|
+
else
|
372
|
+
return result
|
373
|
+
end
|
374
|
+
elsif reply[0] == CMD_EVAL
|
375
|
+
begin
|
376
|
+
result = Object::TOPLEVEL_BINDING.eval(reply[2])
|
377
|
+
send_reply(reply[1], result)
|
378
|
+
rescue => e
|
379
|
+
send_error(reply[1], e)
|
380
|
+
end
|
381
|
+
elsif reply[0] == CMD_CALL
|
382
|
+
begin
|
383
|
+
receiver = decode_obj(reply[2])
|
384
|
+
name = reply[3]
|
385
|
+
args = reply[4].map {|e| decode_obj(e)}
|
386
|
+
result = receiver.public_send(name, *args)
|
387
|
+
send_reply(reply[1], result)
|
388
|
+
rescue => e
|
389
|
+
send_error(reply[1], e)
|
390
|
+
end
|
391
|
+
elsif reply[0] == CMD_RETRY
|
392
|
+
if reply[1] != message_id
|
393
|
+
send_reply(reply[1], nil, false, CMD_REJECT)
|
394
|
+
else
|
395
|
+
result = @pending_replies.delete(message_id)
|
396
|
+
if result.nil?
|
397
|
+
raise RuntimeError.new("bad CMD_RETRY: #{reply}")
|
398
|
+
elsif result.is_a?(JavaScriptError)
|
399
|
+
raise result
|
400
|
+
else
|
401
|
+
return result
|
402
|
+
end
|
403
|
+
end
|
404
|
+
else
|
405
|
+
# CMD_REJECT and other unknown commands
|
406
|
+
raise RuntimeError.new("bad message: #{reply}")
|
287
407
|
end
|
288
|
-
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def send_reply(message_id, value, erroneous = false, cmd_id=CMD_REPLY)
|
412
|
+
if erroneous
|
413
|
+
encoded = encode_eval_error(value)
|
289
414
|
else
|
290
|
-
|
415
|
+
encoded = encode_obj(value)
|
416
|
+
end
|
417
|
+
json_data = JSON.generate(send_with_piggyback([cmd_id, message_id, encoded]))
|
418
|
+
header = (Header_format % json_data.length)
|
419
|
+
if header.length != Header_size
|
420
|
+
raise "message length limit exceeded"
|
291
421
|
end
|
422
|
+
json_data_with_header = header + json_data
|
423
|
+
@pipe.puts(json_data_with_header)
|
424
|
+
end
|
425
|
+
|
426
|
+
def send_error(message_id, e)
|
427
|
+
send_reply(message_id, e, true)
|
292
428
|
end
|
293
429
|
end
|
294
430
|
|
295
431
|
@pipe = nil
|
296
|
-
@
|
297
|
-
@options = ''
|
432
|
+
@configurations = {}
|
298
433
|
@pipeToJsClass = PipeToJs
|
299
434
|
|
300
|
-
def self.config(module_names: [], options: '', browser: false)
|
301
|
-
|
302
|
-
|
435
|
+
#def self.config(module_names: [], options: '', browser: false)
|
436
|
+
def self.config(**kw)
|
437
|
+
if kw.nil? || kw == {}
|
438
|
+
@configurations = {}
|
439
|
+
else
|
440
|
+
@configurations = @configurations.merge!(kw)
|
441
|
+
end
|
442
|
+
browser = @configurations[:browser]
|
303
443
|
@pipeToJsClass = if browser then PipeToBrowser else PipeToJs end
|
304
444
|
nil
|
305
445
|
end
|
@@ -310,35 +450,65 @@ module Jscall
|
|
310
450
|
|
311
451
|
Signal.trap(0) { self.close } # close before termination
|
312
452
|
|
313
|
-
def self.exec(src)
|
314
|
-
__getpipe__.exec(src)
|
315
|
-
end
|
316
|
-
|
317
|
-
def self.dyn_import(name, var_name=nil)
|
318
|
-
__getpipe__.funcall(nil, 'Ruby.dyn_import', [name, var_name])
|
319
|
-
end
|
320
|
-
|
321
|
-
# name is a string object.
|
322
|
-
# Evaluating this string in JavaScript results in a JavaScript function.
|
323
|
-
#
|
324
|
-
def self.funcall(name, *args)
|
325
|
-
__getpipe__.funcall(nil, name, args)
|
326
|
-
end
|
327
|
-
|
328
453
|
# reclaim unused remote references.
|
329
454
|
#
|
330
455
|
def self.scavenge_references
|
331
456
|
__getpipe__.scavenge
|
332
457
|
end
|
333
458
|
|
334
|
-
def self.method_missing(name, *args)
|
335
|
-
__getpipe__.funcall(nil, name, args)
|
336
|
-
end
|
337
|
-
|
338
459
|
def self.__getpipe__
|
339
460
|
if @pipe.nil?
|
340
|
-
@pipe = @pipeToJsClass.new(@
|
461
|
+
@pipe = @pipeToJsClass.new(@configurations)
|
462
|
+
@pipe.setup(@configurations)
|
341
463
|
end
|
342
464
|
@pipe
|
343
465
|
end
|
466
|
+
|
467
|
+
module Interface
|
468
|
+
def exec(src)
|
469
|
+
__getpipe__.exec(src)
|
470
|
+
end
|
471
|
+
|
472
|
+
def async_exec(src)
|
473
|
+
__getpipe__.async_exec(src)
|
474
|
+
end
|
475
|
+
|
476
|
+
# name is a string object.
|
477
|
+
# Evaluating this string in JavaScript results in a JavaScript function.
|
478
|
+
#
|
479
|
+
def funcall(name, *args)
|
480
|
+
__getpipe__.funcall(nil, name, args)
|
481
|
+
end
|
482
|
+
|
483
|
+
def async_funcall(name, *args)
|
484
|
+
__getpipe__.async_funcall(nil, name, args)
|
485
|
+
end
|
486
|
+
|
487
|
+
def dyn_import(name, var_name=nil)
|
488
|
+
funcall('Ruby.dyn_import', name, var_name)
|
489
|
+
end
|
490
|
+
|
491
|
+
def method_missing(name, *args)
|
492
|
+
funcall(name, *args)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
extend Interface
|
497
|
+
|
498
|
+
module AsyncInterface
|
499
|
+
include Interface
|
500
|
+
|
501
|
+
alias exec async_exec
|
502
|
+
alias funcall async_funcall
|
503
|
+
end
|
504
|
+
|
505
|
+
def self.async
|
506
|
+
@async ||= Class.new do
|
507
|
+
def __getpipe__
|
508
|
+
Jscall.__getpipe__
|
509
|
+
end
|
510
|
+
|
511
|
+
include AsyncInterface
|
512
|
+
end.new
|
513
|
+
end
|
344
514
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jscall
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shigeru Chiba
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: webrick
|
@@ -50,7 +50,7 @@ licenses:
|
|
50
50
|
metadata:
|
51
51
|
homepage_uri: https://github.com/csg-tokyo/jscall
|
52
52
|
source_code_uri: https://github.com/csg-tokyo/jscall
|
53
|
-
post_install_message:
|
53
|
+
post_install_message:
|
54
54
|
rdoc_options: []
|
55
55
|
require_paths:
|
56
56
|
- lib
|
@@ -65,8 +65,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
65
|
- !ruby/object:Gem::Version
|
66
66
|
version: '0'
|
67
67
|
requirements: []
|
68
|
-
rubygems_version: 3.
|
69
|
-
signing_key:
|
68
|
+
rubygems_version: 3.3.3
|
69
|
+
signing_key:
|
70
70
|
specification_version: 4
|
71
71
|
summary: a library for calling JavaScript functions
|
72
72
|
test_files: []
|