jscall 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +225 -29
- data/examples/pdf-js.rb +56 -0
- data/lib/jscall/browser.mjs +36 -11
- data/lib/jscall/browser.rb +29 -7
- data/lib/jscall/main.mjs +218 -62
- data/lib/jscall/synch.mjs +185 -0
- data/lib/jscall/version.rb +1 -1
- data/lib/jscall.rb +246 -75
- metadata +8 -6
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,24 +200,39 @@ 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
|
+
main_js_file = if config[:sync] then "synch.mjs" else "main.mjs" end
|
235
|
+
script = "'import * as m1 from \"#{__dir__}/jscall/#{main_js_file}\"; globalThis.Ruby = m1; #{script2}; Ruby.start(process.stdin, true)'"
|
169
236
|
@pipe = IO.popen("#{@@node_cmd} #{options} --input-type 'module' -e #{script}", "r+t")
|
170
237
|
@pipe.autoclose = true
|
171
238
|
end
|
@@ -184,8 +251,12 @@ module Jscall
|
|
184
251
|
obj
|
185
252
|
elsif obj.is_a?(Array)
|
186
253
|
[Param_array, obj.map {|e| encode_obj(e)}]
|
254
|
+
elsif obj.is_a?(Hash)
|
255
|
+
hash2 = {}
|
256
|
+
obj.each {|key, value| hash2[key] = encode_obj(value) }
|
257
|
+
[Param_hash, hash2]
|
187
258
|
elsif obj.is_a?(RemoteRef)
|
188
|
-
[Param_local_object, obj.
|
259
|
+
[Param_local_object, obj.__get_id]
|
189
260
|
else
|
190
261
|
[Param_object, @exported.export(obj)]
|
191
262
|
end
|
@@ -199,11 +270,15 @@ module Jscall
|
|
199
270
|
if obj.is_a?(Numeric) || obj.is_a?(String) || obj == true || obj == false || obj == nil
|
200
271
|
obj
|
201
272
|
elsif obj.is_a?(Array) && obj.size == 2
|
202
|
-
if
|
273
|
+
if obj[0] == Param_array
|
203
274
|
obj[1].map {|e| decode_obj(e)}
|
204
|
-
elsif
|
275
|
+
elsif obj[0] == Param_hash
|
276
|
+
hash = {}
|
277
|
+
obj[1].each {|key, value| hash[key] = decode_obj(value)}
|
278
|
+
hash
|
279
|
+
elsif obj[0] == Param_object
|
205
280
|
@imported.import(obj[1])
|
206
|
-
elsif
|
281
|
+
elsif obj[0] == Param_local_object
|
207
282
|
@exported.find(obj[1])
|
208
283
|
else # if Param_error
|
209
284
|
JavaScriptError.new(obj[1])
|
@@ -213,20 +288,38 @@ module Jscall
|
|
213
288
|
end
|
214
289
|
end
|
215
290
|
|
291
|
+
def fresh_id
|
292
|
+
@num_generated_ids += 1
|
293
|
+
end
|
294
|
+
|
216
295
|
def funcall(receiver, name, args)
|
217
|
-
cmd = [CMD_CALL, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
|
296
|
+
cmd = [CMD_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
|
297
|
+
send_command(cmd)
|
298
|
+
end
|
299
|
+
|
300
|
+
def async_funcall(receiver, name, args)
|
301
|
+
cmd = [CMD_ASYNC_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
|
218
302
|
send_command(cmd)
|
219
303
|
end
|
220
304
|
|
221
305
|
def exec(src)
|
222
|
-
cmd = [CMD_EVAL, src]
|
306
|
+
cmd = [CMD_EVAL, nil, src]
|
307
|
+
send_command(cmd)
|
308
|
+
end
|
309
|
+
|
310
|
+
def async_exec(src)
|
311
|
+
cmd = [CMD_ASYNC_EVAL, nil, src]
|
223
312
|
send_command(cmd)
|
224
313
|
end
|
225
314
|
|
226
315
|
def encode_eval_error(e)
|
227
316
|
traces = e.backtrace
|
228
317
|
location = if traces.size > 0 then traces[0] else '' end
|
229
|
-
|
318
|
+
if Jscall.debug > 0
|
319
|
+
encode_error("\n#{e.full_message}")
|
320
|
+
else
|
321
|
+
encode_error(location + ' ' + e.to_s)
|
322
|
+
end
|
230
323
|
end
|
231
324
|
|
232
325
|
def scavenge
|
@@ -243,7 +336,7 @@ module Jscall
|
|
243
336
|
dead_refs = @imported.dead_references()
|
244
337
|
if (dead_refs.length > 0)
|
245
338
|
cmd2 = cmd.dup
|
246
|
-
cmd2[
|
339
|
+
cmd2[5] = dead_refs
|
247
340
|
return cmd2
|
248
341
|
end
|
249
342
|
end
|
@@ -251,55 +344,103 @@ module Jscall
|
|
251
344
|
end
|
252
345
|
|
253
346
|
def send_command(cmd)
|
347
|
+
message_id = (cmd[1] ||= fresh_id)
|
254
348
|
json_data = JSON.generate(send_with_piggyback(cmd))
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
if reply.length > 4
|
259
|
-
reply[4].each {|idx| @exported.remove(idx) }
|
349
|
+
header = (Header_format % json_data.length)
|
350
|
+
if header.length != Header_size
|
351
|
+
raise "message length limit exceeded"
|
260
352
|
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)
|
353
|
+
json_data_with_header = header + json_data
|
354
|
+
@pipe.puts(json_data_with_header)
|
355
|
+
|
356
|
+
while true
|
357
|
+
reply_data = @pipe.gets
|
358
|
+
reply = JSON.parse(reply_data || '[]')
|
359
|
+
if reply.length > 5
|
360
|
+
reply[5].each {|idx| @exported.remove(idx) }
|
361
|
+
reply[5] = nil
|
276
362
|
end
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
363
|
+
if @pipe.closed?
|
364
|
+
raise RuntimeError.new("connection closed: #{reply}")
|
365
|
+
elsif reply[0] == CMD_REPLY
|
366
|
+
result = decode_obj(reply[2])
|
367
|
+
if reply[1] != message_id
|
368
|
+
@pending_replies[reply[1]] = result
|
369
|
+
send_reply(reply[1], nil, false, CMD_REJECT)
|
370
|
+
elsif result.is_a?(JavaScriptError)
|
371
|
+
raise result
|
372
|
+
else
|
373
|
+
return result
|
374
|
+
end
|
375
|
+
elsif reply[0] == CMD_EVAL
|
376
|
+
begin
|
377
|
+
result = Object::TOPLEVEL_BINDING.eval(reply[2])
|
378
|
+
send_reply(reply[1], result)
|
379
|
+
rescue => e
|
380
|
+
send_error(reply[1], e)
|
381
|
+
end
|
382
|
+
elsif reply[0] == CMD_CALL
|
383
|
+
begin
|
384
|
+
receiver = decode_obj(reply[2])
|
385
|
+
name = reply[3]
|
386
|
+
args = reply[4].map {|e| decode_obj(e)}
|
387
|
+
result = receiver.public_send(name, *args)
|
388
|
+
send_reply(reply[1], result)
|
389
|
+
rescue => e
|
390
|
+
send_error(reply[1], e)
|
391
|
+
end
|
392
|
+
elsif reply[0] == CMD_RETRY
|
393
|
+
if reply[1] != message_id
|
394
|
+
send_reply(reply[1], nil, false, CMD_REJECT)
|
395
|
+
else
|
396
|
+
result = @pending_replies.delete(message_id)
|
397
|
+
if result.nil?
|
398
|
+
raise RuntimeError.new("bad CMD_RETRY: #{reply}")
|
399
|
+
elsif result.is_a?(JavaScriptError)
|
400
|
+
raise result
|
401
|
+
else
|
402
|
+
return result
|
403
|
+
end
|
404
|
+
end
|
405
|
+
else
|
406
|
+
# CMD_REJECT and other unknown commands
|
407
|
+
raise RuntimeError.new("bad message: #{reply}")
|
287
408
|
end
|
288
|
-
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
def send_reply(message_id, value, erroneous = false, cmd_id=CMD_REPLY)
|
413
|
+
if erroneous
|
414
|
+
encoded = encode_eval_error(value)
|
289
415
|
else
|
290
|
-
|
416
|
+
encoded = encode_obj(value)
|
417
|
+
end
|
418
|
+
json_data = JSON.generate(send_with_piggyback([cmd_id, message_id, encoded]))
|
419
|
+
header = (Header_format % json_data.length)
|
420
|
+
if header.length != Header_size
|
421
|
+
raise "message length limit exceeded"
|
291
422
|
end
|
423
|
+
json_data_with_header = header + json_data
|
424
|
+
@pipe.puts(json_data_with_header)
|
425
|
+
end
|
426
|
+
|
427
|
+
def send_error(message_id, e)
|
428
|
+
send_reply(message_id, e, true)
|
292
429
|
end
|
293
430
|
end
|
294
431
|
|
295
432
|
@pipe = nil
|
296
|
-
@
|
297
|
-
@options = ''
|
433
|
+
@configurations = {}
|
298
434
|
@pipeToJsClass = PipeToJs
|
299
435
|
|
300
|
-
def self.config(module_names: [], options: '', browser: false)
|
301
|
-
|
302
|
-
|
436
|
+
#def self.config(module_names: [], options: '', browser: false, sync: false)
|
437
|
+
def self.config(**kw)
|
438
|
+
if kw.nil? || kw == {}
|
439
|
+
@configurations = {}
|
440
|
+
else
|
441
|
+
@configurations = @configurations.merge!(kw)
|
442
|
+
end
|
443
|
+
browser = @configurations[:browser]
|
303
444
|
@pipeToJsClass = if browser then PipeToBrowser else PipeToJs end
|
304
445
|
nil
|
305
446
|
end
|
@@ -310,35 +451,65 @@ module Jscall
|
|
310
451
|
|
311
452
|
Signal.trap(0) { self.close } # close before termination
|
312
453
|
|
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
454
|
# reclaim unused remote references.
|
329
455
|
#
|
330
456
|
def self.scavenge_references
|
331
457
|
__getpipe__.scavenge
|
332
458
|
end
|
333
459
|
|
334
|
-
def self.method_missing(name, *args)
|
335
|
-
__getpipe__.funcall(nil, name, args)
|
336
|
-
end
|
337
|
-
|
338
460
|
def self.__getpipe__
|
339
461
|
if @pipe.nil?
|
340
|
-
@pipe = @pipeToJsClass.new(@
|
462
|
+
@pipe = @pipeToJsClass.new(@configurations)
|
463
|
+
@pipe.setup(@configurations)
|
341
464
|
end
|
342
465
|
@pipe
|
343
466
|
end
|
467
|
+
|
468
|
+
module Interface
|
469
|
+
def exec(src)
|
470
|
+
__getpipe__.exec(src)
|
471
|
+
end
|
472
|
+
|
473
|
+
def async_exec(src)
|
474
|
+
__getpipe__.async_exec(src)
|
475
|
+
end
|
476
|
+
|
477
|
+
# name is a string object.
|
478
|
+
# Evaluating this string in JavaScript results in a JavaScript function.
|
479
|
+
#
|
480
|
+
def funcall(name, *args)
|
481
|
+
__getpipe__.funcall(nil, name, args)
|
482
|
+
end
|
483
|
+
|
484
|
+
def async_funcall(name, *args)
|
485
|
+
__getpipe__.async_funcall(nil, name, args)
|
486
|
+
end
|
487
|
+
|
488
|
+
def dyn_import(name, var_name=nil)
|
489
|
+
funcall('Ruby.dyn_import', name, var_name)
|
490
|
+
end
|
491
|
+
|
492
|
+
def method_missing(name, *args)
|
493
|
+
funcall(name, *args)
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
extend Interface
|
498
|
+
|
499
|
+
module AsyncInterface
|
500
|
+
include Interface
|
501
|
+
|
502
|
+
alias exec async_exec
|
503
|
+
alias funcall async_funcall
|
504
|
+
end
|
505
|
+
|
506
|
+
def self.async
|
507
|
+
@async ||= Class.new do
|
508
|
+
def __getpipe__
|
509
|
+
Jscall.__getpipe__
|
510
|
+
end
|
511
|
+
|
512
|
+
include AsyncInterface
|
513
|
+
end.new
|
514
|
+
end
|
344
515
|
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.2.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-09-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: webrick
|
@@ -35,6 +35,7 @@ files:
|
|
35
35
|
- LICENSE
|
36
36
|
- README.md
|
37
37
|
- Rakefile
|
38
|
+
- examples/pdf-js.rb
|
38
39
|
- jscall.gemspec
|
39
40
|
- lib/jscall.rb
|
40
41
|
- lib/jscall/apple-touch-icon.png
|
@@ -43,6 +44,7 @@ files:
|
|
43
44
|
- lib/jscall/favicon.ico
|
44
45
|
- lib/jscall/jscall.html
|
45
46
|
- lib/jscall/main.mjs
|
47
|
+
- lib/jscall/synch.mjs
|
46
48
|
- lib/jscall/version.rb
|
47
49
|
homepage: https://github.com/csg-tokyo/jscall
|
48
50
|
licenses:
|
@@ -50,7 +52,7 @@ licenses:
|
|
50
52
|
metadata:
|
51
53
|
homepage_uri: https://github.com/csg-tokyo/jscall
|
52
54
|
source_code_uri: https://github.com/csg-tokyo/jscall
|
53
|
-
post_install_message:
|
55
|
+
post_install_message:
|
54
56
|
rdoc_options: []
|
55
57
|
require_paths:
|
56
58
|
- lib
|
@@ -65,8 +67,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
67
|
- !ruby/object:Gem::Version
|
66
68
|
version: '0'
|
67
69
|
requirements: []
|
68
|
-
rubygems_version: 3.
|
69
|
-
signing_key:
|
70
|
+
rubygems_version: 3.3.3
|
71
|
+
signing_key:
|
70
72
|
specification_version: 4
|
71
73
|
summary: a library for calling JavaScript functions
|
72
74
|
test_files: []
|