Class: Jscall::PipeToJs

Inherits:
Object
  • Object
show all
Defined in:
lib/jscall.rb,
lib/jscall/browser.rb

Direct Known Subclasses

PipeToBrowser

Constant Summary collapse

CMD_EVAL =
1
CMD_CALL =
2
CMD_REPLY =
3
CMD_ASYNC_CALL =
4
CMD_ASYNC_EVAL =
5
CMD_RETRY =
6
CMD_REJECT =
7
Param_array =
0
Param_object =
1
Param_local_object =
2
Param_error =
3
Param_hash =
4
Header_size =
6
Header_format =
'%%0%dx' % Header_size
@@node_cmd =
'node'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ PipeToJs

Returns a new instance of PipeToJs.



203
204
205
206
207
208
209
210
211
# File 'lib/jscall.rb', line 203

def initialize(config)
    @exported = Exported.new
    @imported = Imported.new
    @send_counter = 0
    @num_generated_ids = 0
    @pending_replies = {}
    module_names = config[:module_names] || []
    startJS(module_names, config)
end

Class Method Details

.node_command=(cmd) ⇒ Object



199
200
201
# File 'lib/jscall.rb', line 199

def self.node_command=(cmd)
    @@node_cmd = cmd
end

Instance Method Details

#async_exec(src) ⇒ Object



310
311
312
313
# File 'lib/jscall.rb', line 310

def async_exec(src)
    cmd = [CMD_ASYNC_EVAL, nil, src]
    send_command(cmd)
end

#async_funcall(receiver, name, args) ⇒ Object



300
301
302
303
# File 'lib/jscall.rb', line 300

def async_funcall(receiver, name, args)
    cmd = [CMD_ASYNC_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
    send_command(cmd)
end

#closeObject



244
245
246
247
# File 'lib/jscall.rb', line 244

def close
    @pipe.close
    true
end

#decode_obj(obj) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/jscall.rb', line 269

def decode_obj(obj)
    if obj.is_a?(Numeric) || obj.is_a?(String) || obj == true || obj == false || obj == nil
        obj
    elsif obj.is_a?(Array) && obj.size == 2
        if obj[0] == Param_array
            obj[1].map {|e| decode_obj(e)}
        elsif obj[0] == Param_hash
            hash = {}
            obj[1].each {|key, value| hash[key] = decode_obj(value)}
            hash
        elsif obj[0] == Param_object
            @imported.import(obj[1])
        elsif obj[0] == Param_local_object
            @exported.find(obj[1])
        else  # if Param_error
            JavaScriptError.new(obj[1])
        end
    else
        raise JavaScriptError.new('the result is a broken value')
    end
end

#encode_error(msg) ⇒ Object



265
266
267
# File 'lib/jscall.rb', line 265

def encode_error(msg)
    [Param_error, msg]
end

#encode_eval_error(e) ⇒ Object



315
316
317
318
319
320
321
322
323
# File 'lib/jscall.rb', line 315

def encode_eval_error(e)
    traces = e.backtrace
    location = if traces.size > 0 then traces[0] else '' end
    if Jscall.debug > 0
        encode_error("\n#{e.full_message}")
    else
        encode_error(location + ' ' + e.to_s)
    end
end

#encode_obj(obj) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/jscall.rb', line 249

def encode_obj(obj)
    if obj.is_a?(Numeric) || obj.is_a?(String) || obj.is_a?(Symbol) || obj == true || obj == false || obj == nil
        obj
    elsif obj.is_a?(Array)
        [Param_array, obj.map {|e| encode_obj(e)}]
    elsif obj.is_a?(Hash)
        hash2 = {}
        obj.each {|key, value| hash2[key] = encode_obj(value) }
        [Param_hash, hash2]
    elsif obj.is_a?(RemoteRef)
        [Param_local_object, obj.__get_id]
    else
        [Param_object, @exported.export(obj)]
    end
end

#exec(src) ⇒ Object



305
306
307
308
# File 'lib/jscall.rb', line 305

def exec(src)
    cmd = [CMD_EVAL, nil, src]
    send_command(cmd)
end

#fresh_idObject



291
292
293
# File 'lib/jscall.rb', line 291

def fresh_id
    @num_generated_ids += 1
end

#funcall(receiver, name, args) ⇒ Object



295
296
297
298
# File 'lib/jscall.rb', line 295

def funcall(receiver, name, args)
    cmd = [CMD_CALL, nil, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
    send_command(cmd)
end

#get_exported_importedObject



240
241
242
# File 'lib/jscall.rb', line 240

def get_exported_imported
    [@exported, @imported]
end

#scavengeObject



325
326
327
328
329
# File 'lib/jscall.rb', line 325

def scavenge
    @send_counter = 200
    @imported.kill_canary
    exec 'Ruby.scavenge_references()'
end

#send_command(cmd) ⇒ Object



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/jscall.rb', line 346

def send_command(cmd)
    message_id = (cmd[1] ||= fresh_id)
    json_data = JSON.generate(send_with_piggyback(cmd))
    header = (Header_format % json_data.length)
    if header.length != Header_size
        raise "message length limit exceeded"
    end
    json_data_with_header = header + json_data
    @pipe.puts(json_data_with_header)

    while true
        reply_data = @pipe.gets
        reply = JSON.parse(reply_data || '[]')
        if reply.length > 5
            reply[5].each {|idx| @exported.remove(idx) }
            reply[5] = nil
        end
        if @pipe.closed?
            raise RuntimeError.new("connection closed: #{reply}")
        elsif reply[0] == CMD_REPLY
            result = decode_obj(reply[2])
            if reply[1] != message_id
                @pending_replies[reply[1]] = result
                send_reply(reply[1], nil, false, CMD_REJECT)
            elsif result.is_a?(JavaScriptError)
                raise result
            else
                return result
            end
        elsif reply[0] == CMD_EVAL
            begin
                result = Object::TOPLEVEL_BINDING.eval(reply[2])
                send_reply(reply[1], result)
            rescue => e
                send_error(reply[1], e)
            end
        elsif reply[0] == CMD_CALL
            begin
                receiver = decode_obj(reply[2])
                name = reply[3]
                args = reply[4].map {|e| decode_obj(e)}
                result = receiver.public_send(name, *args)
                send_reply(reply[1], result)
            rescue => e
                send_error(reply[1], e)
            end
        elsif reply[0] == CMD_RETRY
            if reply[1] != message_id
                send_reply(reply[1], nil, false, CMD_REJECT)
            else
                if @pending_replies.key?(message_id)
                    result = @pending_replies.delete(message_id)
                    if result.is_a?(JavaScriptError)
                        raise result
                    else
                        return result
                    end
                else
                    raise RuntimeError.new("bad CMD_RETRY: #{reply}")
                end
            end
        else
            # CMD_REJECT and other unknown commands
            raise RuntimeError.new("bad message: #{reply}")
        end
    end
end

#send_error(message_id, e) ⇒ Object



429
430
431
# File 'lib/jscall.rb', line 429

def send_error(message_id, e)
    send_reply(message_id, e, true)
end

#send_reply(message_id, value, erroneous = false, cmd_id = CMD_REPLY) ⇒ Object



414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/jscall.rb', line 414

def send_reply(message_id, value, erroneous = false, cmd_id=CMD_REPLY)
    if erroneous
        encoded = encode_eval_error(value)
    else
        encoded = encode_obj(value)
    end
    json_data = JSON.generate(send_with_piggyback([cmd_id, message_id, encoded]))
    header = (Header_format % json_data.length)
    if header.length != Header_size
        raise "message length limit exceeded"
    end
    json_data_with_header = header + json_data
    @pipe.puts(json_data_with_header)
end

#send_with_piggyback(cmd) ⇒ Object



331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/jscall.rb', line 331

def send_with_piggyback(cmd)
    threashold = 100
    @send_counter += 1
    if (@send_counter > threashold)
        @send_counter = 0
        dead_refs = @imported.dead_references()
        if (dead_refs.length > 0)
            cmd2 = cmd.dup
            cmd2[5] = dead_refs
            return cmd2
        end
    end
    return cmd
end

#setup(config) ⇒ Object



213
214
215
# File 'lib/jscall.rb', line 213

def setup(config)
    # called just after executing new PipeToJs(config)
end

#startJS(module_names, config) ⇒ Object

Config options.

module_names: an array of [module_name, module_root, module_file_name]

For example,
  [['Foo', '/home/jscall', '/lib/foo.mjs']]
this does
  import * as Foo from "/home/jscall/lib/foo.mjs"

options: options passed to node.js



227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/jscall.rb', line 227

def startJS(module_names, config)
    options = config[:options] || ''
    script2 = ''
    module_names.each_index do |i|
        script2 += "import * as m#{i + 2} from \"#{module_names[i][1]}#{module_names[i][2]}\"; globalThis.#{module_names[i][0]} = m#{i + 2}; "
    end
    script2 += "import { createRequire } from \"node:module\"; globalThis.require = createRequire(\"file://#{Dir.pwd}/\");"
    main_js_file = if config[:sync] then "synch.mjs" else "main.mjs" end
    script = "'import * as m1 from \"#{__dir__}/jscall/#{main_js_file}\"; globalThis.Ruby = m1; #{script2}; Ruby.start(process.stdin, true)'"
    @pipe = IO.popen("#{@@node_cmd} #{options} --input-type 'module' -e #{script}", "r+t")
    @pipe.autoclose = true
end