quickjs.rb

A Ruby wrapper for QuickJS to run JavaScript codes via Ruby with a smaller footprint.

Gem Version GitHub Actions Workflow Status

Installation

gem install quickjs
gem 'quickjs'

Usage

Quickjs.eval_code: Evaluate JavaScript code instantly

require 'quickjs'

Quickjs.eval_code('const fn = (n, pow) => n ** pow; fn(2,8);') # => 256
Quickjs.eval_code('const fn = (name) => `Hi, ${name}!`; fn("Itadori");') # => "Hi, Itadori!"
Quickjs.eval_code("[1,2,3]") #=> [1, 2, 3]
Quickjs.eval_code("({ a: '1', b: 1 })") #=> { 'a' => '1', 'b' => 1 }
Options #### Resources ```rb Quickjs.eval_code(code, memory_limit: 1024 ** 3, # 1GB memory limit max_stack_size: 1024 ** 2, # 1MB max stack size ) ``` #### Timeout ```rb # eval_code will be interrupted after 1 sec (default: 100 msec) Quickjs.eval_code(code, timeout_msec: 1_000) ``` #### Features ```rb Quickjs.eval_code(code, features: [::Quickjs::MODULE_STD, ::Quickjs::POLYFILL_FILE]) ``` | Constant | Description | |---|---| | `MODULE_STD` | QuickJS [`std` module](https://bellard.org/quickjs/quickjs.html#std-module) | | `MODULE_OS` | QuickJS [`os` module](https://bellard.org/quickjs/quickjs.html#os-module) | | `FEATURE_TIMEOUT` | `setTimeout` / `setInterval` managed by CRuby | | `POLYFILL_INTL` | Intl API (DateTimeFormat, NumberFormat, PluralRules, Locale) | | `POLYFILL_FILE` | W3C File API (Blob and File) | | `POLYFILL_ENCODING` | Encoding API (TextEncoder and TextDecoder) | | `POLYFILL_URL` | URL API (URL and URLSearchParams) | | `POLYFILL_CRYPTO` | Web Crypto API (`crypto.getRandomValues`, `crypto.randomUUID`, `crypto.subtle`); combine with `POLYFILL_ENCODING` for string↔buffer conversion |

Quickjs::VM: Maintain a consistent VM/runtime

Accepts the same options as Quickjs.eval_code.

vm = Quickjs::VM.new
vm.eval_code('const a = { b: "c" };')
vm.eval_code('a.b;') #=> "c"
vm.eval_code('a.b = "d";')
vm.eval_code('a.b;') #=> "d"

Quickjs::VM#call: ⚑ Call a JS function directly with Ruby arguments

vm = Quickjs::VM.new
vm.eval_code('function add(a, b) { return a + b; }')

vm.call('add', 1, 2)           #=> 3
vm.call(:add, 1, 2)            #=> 3  (Symbol also works)

# Nested functions β€” preserves `this` binding
vm.eval_code('const counter = { n: 0, inc() { return ++this.n; } }')
vm.call('counter.inc')         #=> 1
vm.call('counter.inc')         #=> 2

# Keys with special characters via bracket notation
vm.eval_code("const obj = {}; obj['my-fn'] = x => x * 2;")
vm.call('obj["my-fn"]', 21)    #=> 42

# Async functions are automatically awaited
vm.eval_code('async function fetchVal() { return 42; }')
vm.call('fetchVal')            #=> 42

Quickjs::VM#import: πŸ”Œ Import ESM from a source code

vm = Quickjs::VM.new

# Equivalent to `import { default: aliasedDefault, member: member } from './exports.esm.js';`
vm.import({ default: 'aliasedDefault', member: 'member' }, from: File.read('exports.esm.js'))

vm.eval_code("aliasedDefault()") #=> Exported `default` of the ESM is called
vm.eval_code("member()") #=> Exported `member` of the ESM is called

# import { member, defaultMember } from './exports.esm.js';
vm.import(['member', 'defaultMember'], from: File.read('exports.esm.js'))

# import DefaultExport from './exports.esm.js';
vm.import('DefaultExport', from: File.read('exports.esm.js'))

# import * as all from './exports.esm.js';
vm.import('* as all', from: File.read('exports.esm.js'))

Quickjs::VM#define_function: πŸ’Ž Define a global function for JS by Ruby

vm = Quickjs::VM.new
vm.define_function("greetingTo") do |arg1|
  ['Hello!', arg1].join(' ')
end

vm.eval_code("greetingTo('Rick')") #=> 'Hello! Rick'

A Ruby exception raised inside the block is catchable in JS as an Error, and propagates back to Ruby as the original exception type if uncaught in JS.

vm.define_function("fail") { raise IOError, "something went wrong" }

vm.eval_code('try { fail() } catch (e) { e.message }') #=> "something went wrong"
vm.eval_code("fail()") #=> raise IOError transparently

With POLYFILL_FILE enabled, a Ruby ::File returned from the block becomes a JS File-compatible proxy. Passing it back to Ruby from JS returns the original ::File object.

vm = Quickjs::VM.new(features: [::Quickjs::POLYFILL_FILE])
vm.define_function(:get_file) { File.open('report.pdf') }

vm.eval_code("get_file().name")          #=> "report.pdf"
vm.eval_code("get_file().size")          #=> Integer (byte size)
vm.eval_code("await get_file().text()") #=> file content as String

Quickjs::VM#on_log: πŸ“‘ Handle console logs in real time

Register a block to be called for each console.(log|info|debug|warn|error) call.

vm = Quickjs::VM.new
vm.on_log { |log| puts "#{log.severity}: #{log.to_s}" }

vm.eval_code('console.log("hello", 42)')
# => prints: info: hello 42

# log.severity #=> :info / :verbose / :warning / :error
# log.to_s     #=> space-joined string of all arguments
# log.raw      #=> Array of raw Ruby values

Value Conversion

JavaScript Ruby Note
number (integer / float) ↔ Integer / Float
string ↔ String
true / false ↔ true / false
null ↔ nil
Array ↔ Array via JSON
Object ↔ Hash via JSON
undefined β†’ Quickjs::Value::UNDEFINED
NaN β†’ Quickjs::Value::NAN
Blob β†’ Quickjs::Blob β€” .size, .type, .content requires POLYFILL_FILE
File β†’ Quickjs::File β€” .name, .last_modified + Blob attrs requires POLYFILL_FILE
File proxy ← ::File requires POLYFILL_FILE; applies to define_function return values

License

Otherwise, the MIT License, Copyright 2024 by Kengo Hamasaki.