Skip to content

A Ruby library for calling JavaScript functions on node.js and web browsers

License

Notifications You must be signed in to change notification settings

csg-tokyo/jscall

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

76 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jscall

Ruby

Jscall allows executing a program in JavaScript on node.js or a web browser. By default, node.js is used for the execution. To choose a web browser, call Jscall.config.

Jscall.config(browser: true)

To run JavaScript code, call Jscall.exec. For example,

Jscall.exec '1 + 1'

This returns 2. The argument passed to Jscall.exec can be multiple lines. It is executed as source code written in JavaScript.

Jscall.exec returns a resulting value. Numbers, character strings (and symbols), boolean values, and nil (and null) are copied when passing between Ruby and JavaScript. An array is shallow-copied. Other objects are not copied. When they are passed, a remote reference is created at the destination. When a Map object is returned from JavaScript to Ruby, it is also shallow-copied but into a Hash object in Ruby.

A remote reference is a local reference to a proxy object. A method call on a remote reference invokes a method on the corresponding object on the remote site. For example,

js_obj = Jscall.exec '({ foo: (x) => x + 1, bar: 7 })'
js_obj.foo(3)    # 4
js_obj.bar       # 7
js_obj.baz = 9
js_obj.baz       # 9

The foo method is executed in JavaScript. Since bar is not a function, its value is returned to Ruby as it is.

Setting an object property to a given value is also allowed. The expression js_obj.baz = 9 above sets the object property baz to 9.

An argument to a JavaScript method is copied from Ruby to JavaScript unless it is an object. When an argument is a Ruby object, a proxy object is created in JavaScript. The rule is the same as the rule for returning a value from JavaScript to Ruby. A primitive value is copied but an object is not. An array is shallow-copied.

A Hash object in Ruby is also shallow-copied into JavaScript but a normal object is created in JavaScript. Recall that a JavaScript object is regarded as an associative array, or a hash table as in Ruby. For example, in Ruby,

obj = { a: 2, b: 3 }

when this ruby object is passed to JavaScript as an argument, a normal object { a: 2, b: 3 } is created as its copy in JavaScript and passed to a JavaScript method.

To call a JavaScript function from Ruby, call a method on Jscall. For example,

Jscall.exec <<CODE
  function foo(x) {
    return x + 1
  }
CODE
Jscall.foo(7)    # 8

Jscall.foo(7) invokes the JavaScript function with the name following Jscall. with the given argument. In this case, the foo function is executed with the argument 7. Arguments and a return value are passed to/from a function as they are passed to/from a method.

Jscall can be used for obtaining a remote reference to access a global variable in JavaScript. For example,

Jscall.console.log('Hello')

This prints Hello on a JavaScript console. Jscall.console returns a remote reference to the value of console in JavaScript. Then, .log('Hello') calls the log method on console in JavaScript.

When a Ruby object is passed to a JavaScript function/method, you can call a method on the passed Ruby object.

Jscall.exec <<CODE
  async function foo(obj) {
    return await obj.to_a()
  }
CODE
Jscall.foo((1..3))    # [1, 2, 3]

Here, obj.to_a() calls the to_a method on a Range object created in Ruby. Note that you must await every call to Ruby object since it is asynchronous call.

A shorthand for obj.to_a() is obj.to_a in Ruby. However, this shorthand is not available in JavaScript. You must explicitly write obj.to_a() in JavaScript when obj is a Ruby object.

In JavaScript, Ruby.exec is available to run a program in Ruby. For example,

Jscall.exec <<CODE
  async function foo() {
    return await Ruby.exec('RUBY_VERSION')
  }
CODE
Jscall.foo()

Jscall.foo() returns the result of evaluating given Ruby code RUBY_VERSION in Ruby. Don't forget to await a call to Ruby.exec.

Remote references

A remote reference is implemented by a local reference to a proxy object representing the remote object that the remote reference refers to. When a proxy object is passed as an argument or a return value from Ruby to JavaScript (or vice versa), the corresponding JavaScript (or Ruby) object is passed to the destination. In other words, a remote reference passed is converted back to a local reference.

Remote references will be automatically reclaimed when they are no longer used. To reclaim them immediately, call:

Jscall.scavenge_references

As mentioned above, a remote reference is a local reference to a proxy object. In Ruby, even a proxy object provides a number of methods inherited from Object class, such as clone, to_s, and inspect. A call to such a method is not delegated to the corresponding JavaScript object. To invoke such a method on a JavaScript object, call send on its proxy object. For example,

js_obj = Jscall.exec '({ to_s: (x, y) => x + y })'
puts js_obj.to_s(3, 4)            # error
puts js_obj.send('to_s', 3, 4)    # 7

The send method invokes the JavaScript method with the name specified by the first argument. The remaining arguments passed to send are passed to that JavaScript method.

DOM manipulation

When JavaScript code is run on a browser, some utility methods are available in Ruby for manipulating DOM objects.

  • Jscall.dom.append_css(css_file_path)

This adds a link element to the DOM tree so that the specified css file will be linked. For example, append_css('/mystyle.css') links mystyle.css in the current directory.

  • Jscall.dom.print(msg)

This adds a p element to the DOM tree. Its inner text is the character string passed as msg.

  • Jscall.dom.append_to_body(html_source)

This inserts the given html_source at the end of the body element. It is a shorthand for

Jscall.document.body.insertAdjacentHTML('beforeend', html_source)

Variable scope

Since Jscall uses eval to execute JavaScript code, the scope of variable/constant names is within the code passed to eval. For example,

Jscall.exec 'const k = 3'
Jscall.exec 'k + 3'         # Can't find variable: k

The second line causes an error. k is not visible when 'k + 3' is executed.

To avoid this, use a global variable.

Jscall.exec 'k = 3'
Jscall.exec 'globalThis.j = 4'
Jscall.exec 'k + j'             # 7

Loading a module

When JavaScript code is executed on node.js, require is available in JavaScript for loading a CommonJS module. For example,

Jscall.exec "mime = require('./mime.js')"

The file ./mime.js is loaded and the module is bound to a global variable mime in JavaScript.

You can directly call require on Jscall in Ruby.

parser = Jscall.require("@babel/parser")
ast = parser.parse('const i = 3')
Jscall.console.log(ast)

require will search ./node_modules/ for @babel/parser. This is equivalent to the following JavaScript code.

parser = require("@babel/parser")
ast = parser.parse('const i = 3')
console.log(ast)

Dynamic importing is also available. Call Jscall.dyn_import in Ruby.

fs = Jscall.dyn_import('fs')

This executes dynamic importing in JavaScript. 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.

Jscall.dyn_import takes the second argument. If it is given, a global variable in JavaScript is bound to the loaded module.

fs = Jscall.dyn_import('fs', 'fs_module')

This is quite equivalent to the following JavaScript code:

fs_module = await load('fs')

Promise

If a program attempts to pass a Promise object from JavaScript to Ruby, it waits until the promise is fulfilled. Then Jscall passes the value of that promise from JavaScript to Ruby instead of that promise itself (or a remote reference to that promise). When that promise is rejected, an error object is passed to Ruby so that the error will be raised in Ruby. This design reflects the fact that an async function in JavaScript also returns a Promise object but this object must not be returned to Ruby as is when that async function is called from Ruby. Jscall cannot determine whether a promise should be passed as is to Ruby or its value must be passed to Ruby after the promise is fulfilled.

When enforcing Jscall to pass a Promise object from JavaScript to Ruby, .async must be inserted between a receiver and a method name.

Jscall.exec(<<CODE)
  function make_promise() {
    return { a: Promise.resolve(7) }
  }
CODE

obj = Jscall.make_promise
result = obj.a                # 7
prom = obj.async.a            # promise
prom.then(->(r) { puts r })   # 7

Synchronous calls

You might want to avoid writing await when you call a method on a Ruby object or you execute Ruby code by Ruby.exec from JavaScript. For example, that call is included in library code and you might not be able to modify the library code so that await will be inserted.

Jscall supports synchronous calls from JavaScript to Ruby only when the underlying JavaScript engine is node.js on Linux. In the mode of synchronous calls, you do not have to await a method call on a Ruby object or a call to Ruby.exec. It blocks until the return value comes back from Ruby. While it blocks, all calls from Ruby to JavaScript are synchronously processed.

To change to the mode of synchronous calls, call Jscall.config:

Jscall.config(sync: true)

Configuration

Jscall supports several configuration options. Call Jscall.config with necessary options.

module_names:

To import JavaScript modules when node.js or a web browser starts,

Jscall.config(module_names: [["Foo", "./js", "/lib/foo.mjs"], ["Bar", "./js", "/lib/bar.mjs"]])

This specifies that ./js/lib/foo.mjs and ./js/lib/bar.mjs are imported at the beginning. This is equivalent to the following import declarations:

import * as "Foo" from "./js/lib/foo.mjs"
import * as "Bar" from "./js/lib/bar.mjs"

Note that each array element given to module_names: is

[<module_name> <root> <path>]

<path> must start with /. It is used as a part of the URL when a browser accesses a module. When importing a module for node.js, <root> and <path> are concatenated to form a full path name.

<path> must not start with /jscall or /cmd. They are reserved for internal use.

options:

To specify a command line argument passed to node.js,

Jscall.config(options: '--use-strict')

This call specifies that --use-strict is passed as a command line argument.

browser: and port:

When running JavaScript code on a web browser,

Jscall.config(browser: true, port: 10082)

Passing true for browser: switches the execution engine to a web browser. The default engine is node.js. To switch the engine back to node.js, pass false for browser:. Call Jscall.close to detach the current execution engine. A new engine with a new configuration will be created.

port: specifies the port number of an http server. It is optional. The example above specifies that Ruby receives http requests sent to http://localhost:10082 from JavaScript on a web browser.

Misc.

To change to the mode of synchronous calls,

Jscall.config(sync: true)

To set all the configurations to the default ones,

Jscall.config()

Other configurations

To obtain more detailed error messages, set a debugging level to 10. In Ruby,

Jscall.debug = 10

In JavaScript,

Ruby.setDebugLevel(10)

The default debugging level is 0.

To change the name of the node command,

Jscall::PipeToJs.node_command = "node.exe"

The default command name is "node".

To change the command for launching a web browser,

Jscall::FetchServer.open_command = "open -a '/Applications/Safari.app'"

By default, the command name is open for macOS, start for Windows, or xdg-open for Linux. Jscall launches a web browser by the command like the following:

open http://localhost:10082/jscall/jscall.html

Jscall generates a verbose error message if its debug level is more than 0.

Jscall.debug = 1

Installation

Add this line to your application's Gemfile:

gem 'jscall'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install jscall

Development

After checking out the repo, run bin/setup to install dependencies. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/csg-tokyo/jscall.

License

The gem is available as open source under the terms of the MIT License.

Acknowledgment

The icon image for jscall was created by partly using the Ruby logo, which was obtained from https://www.ruby-lang.org/en/about/logo/ under CC BY-SA 2.5.

About

A Ruby library for calling JavaScript functions on node.js and web browsers

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages