Values and calls
Every value that crosses between Rust and JavaScript is a Value. Primitives are copied out of the engine as Rust data; objects and symbols stay in the engine's heap and Rust holds handles to them. This page shows each kind, then how to read and create objects, call functions and convert values. The code comes from examples/values.rs, which asserts every result shown.
All types on this page are in quickjs_oxide::engine::api::core.
The Value enum
pub enum Value {
Undefined,
Null,
Bool(bool),
Int(i32),
Float(f64),
BigInt(JsBigInt),
String(JsString),
Symbol(SymbolRef),
Object(ObjectRef),
}A JavaScript number is Int when it is an integer in i32 range other than -0, and Float otherwise. Value::number(f64) makes the right one, and as_number() reads either as f64.
== on Value (PartialEq) compares representations: Int(1) and Float(1.0) differ, and two ObjectRefs are equal when they name the same object. For JavaScript's comparisons use strict_equal (===), same_value (Object.is) and same_value_zero.
assert_eq!(context.eval("undefined")?, Value::Undefined);
assert_eq!(context.eval("null")?, Value::Null);
assert_eq!(context.eval("!0")?, Value::Bool(true));
assert_eq!(context.eval("2 ** 10")?, Value::Int(1024));
assert_eq!(context.eval("1 / 3")?, Value::Float(1.0 / 3.0));
assert_eq!(context.eval("-0")?, Value::Float(-0.0));
// Value::number picks Int for integers in i32 range and Float otherwise.
assert_eq!(Value::number(7.0), Value::Int(7));
assert_eq!(Value::number(2.5), Value::Float(2.5));
// Equality as JavaScript defines it.
assert!(Value::Int(1).strict_equal(&Value::Float(1.0)));
assert!(!Value::Float(f64::NAN).strict_equal(&Value::Float(f64::NAN)));
assert!(Value::Float(f64::NAN).same_value(&Value::Float(f64::NAN)));
// ToBoolean and typeof for primitives never run JavaScript.
assert!(!Value::Int(0).to_boolean());
assert_eq!(Value::Null.type_of(), "object");Value::type_of answers from the variant alone, so every object is "object". Runtime::type_of asks the engine and reports "function" for callable objects.
From conversions build values from bool, i32, f64 (through Value::number), JsString, ObjectRef and CallableRef.
Strings
JsString holds UTF-16 code units, as JavaScript strings do. It can hold lone surrogates, which UTF-8 cannot, so conversion to a Rust String is lossy: to_utf8_lossy replaces each lone surrogate with U+FFFD.
let text = JsString::try_from_utf8("héllo 😀")?;
// Length counts UTF-16 code units, as `"héllo 😀".length` does.
assert_eq!(text.len(), 8);
assert_eq!(text.code_unit_at(1), Some(0x00e9));
assert_eq!(text.to_utf8_lossy(), "héllo 😀");
// A lone surrogate is a valid JavaScript string but not valid UTF-8.
let lone = JsString::try_from_utf16([0xd800])?;
assert!(!lone.is_well_formed());
assert_eq!(lone.to_utf8_lossy(), "\u{fffd}");
let constant = JsString::from_static("ready");
assert_eq!(
Value::from(constant),
Value::String(JsString::try_from_utf8("ready")?)
);| Method | Result |
|---|---|
JsString::try_from_utf8(&str) |
a string from UTF-8 |
JsString::try_from_utf16(units) |
a string from UTF-16 code units |
JsString::from_static(&'static str) |
a string from a constant |
len(), is_empty() |
length in UTF-16 code units |
code_unit_at(i), code_point_at(i) |
one code unit or code point |
utf16_units() |
an iterator over the code units |
is_well_formed() |
false when the string has a lone surrogate |
to_utf8_lossy() |
a Rust String |
BigInts
JsBigInt is an arbitrary-precision integer. It implements Display, FromStr and From for the Rust integer types.
let Value::BigInt(big) = context.eval("2n ** 70n")? else {
panic!("expected a BigInt");
};
assert_eq!(big.to_string(), "1180591620717411303424");
assert_eq!(big.as_i64(), None); // does not fit in an i64
let small = JsBigInt::from(-5_i64);
assert_eq!(small.as_i64(), Some(-5));
assert_eq!(small.to_string_radix(16)?, "-5");Objects, functions and symbols
Objects and symbols are handles into the engine's heap:
| Type | Holds |
|---|---|
ObjectRef |
any object: plain objects, arrays, functions, errors, promises |
CallableRef |
an object known to be callable; it dereferences to ObjectRef |
SymbolRef |
a symbol |
A handle keeps its value alive until the last clone of the handle drops. Cloning a handle is cheap and names the same object. A handle belongs to the runtime that made it (belongs_to(&runtime)); passing it to another runtime returns RuntimeError::Invariant.
Runtime::as_callable(&object) returns Some(CallableRef) when the object is callable, and Runtime::is_constructor tells whether it accepts new. CallableRef::as_object and into_object give back the ObjectRef.
Symbols are made with Runtime::new_symbol, read with Runtime::symbol_description, and used as property keys through PropertyKey::from(symbol). Runtime::well_known_symbol returns the symbols JavaScript sees as Symbol.iterator, Symbol.asyncIterator and the others listed by WellKnownSymbol::ALL.
let tag = runtime.new_symbol(Some(JsString::try_from_utf8("tag")?))?;
let description = runtime.symbol_description(&tag)?.expect("a description");
assert_eq!(description.to_utf8_lossy(), "tag");
// A symbol is a property key.
let object = context.new_object()?;
let key = PropertyKey::from(tag.clone());
context.set_property(&object, &key, Value::Int(1))?;
assert_eq!(context.get_property(&object, &key)?, Value::Int(1));
// The well-known symbols are the same ones JavaScript sees.
let iterator = runtime.well_known_symbol(WellKnownSymbol::Iterator)?;
assert_eq!(context.eval("Symbol.iterator")?, Value::Symbol(iterator));Read properties
A property name is a PropertyKey: a string, an array index or a symbol. Runtime::intern_property_key("name") makes a string key (or an index key when the text is an array index such as "0"), and PropertyKey::index(n) makes an index key directly.
Context::get_property runs the full [[Get]], including getters and proxies, and returns Value::Undefined for a missing property. Runtime::has_property and has_own_property test presence, and own_property_keys lists own keys in JavaScript's property order.
let Value::Object(user) = context.eval("({ name: 'Ada', langs: ['en', 'fr'] })")? else {
panic!("expected an object");
};
let name_key = runtime.intern_property_key("name")?;
let name = context.get_property(&user, &name_key)?;
assert_eq!(context.to_js_string(&name)?.to_utf8_lossy(), "Ada");
// A missing property reads as undefined.
let age_key = runtime.intern_property_key("age")?;
assert_eq!(context.get_property(&user, &age_key)?, Value::Undefined);
assert!(!runtime.has_property(&user, &age_key)?);
// Array elements use index keys.
let langs_key = runtime.intern_property_key("langs")?;
let Value::Object(langs) = context.get_property(&user, &langs_key)? else {
panic!("expected an array");
};
assert!(runtime.is_array_object(&langs)?);
let second = context.get_property(&langs, &PropertyKey::index(1))?;
assert_eq!(context.to_js_string(&second)?.to_utf8_lossy(), "fr");
// Own keys in property order.
let keys: Vec<String> = runtime
.own_property_keys(&user)?
.iter()
.filter_map(|key| key.to_js_string())
.map(|key| key.to_utf8_lossy())
.collect();
assert_eq!(keys, ["name", "langs"]);Context::get_own_property returns an own property's full descriptor (CompleteOrdinaryPropertyDescriptor::Data or Accessor); an accessor's getter is returned as a CallableRef, not called. Runtime::delete_property deletes, and get_prototype_of and set_prototype_of read and change the prototype.
Create objects and arrays
| Method | Creates |
|---|---|
Context::new_object() |
{} with Object.prototype |
Context::new_object_with_prototype(Some(&proto)) |
an object with the given prototype, or none with None |
Context::new_array() |
[] |
Context::new_array_from_values(vec) |
an array of the given values |
Context::new_native_error(kind, message) |
an error object such as a TypeError, not thrown |
Context::set_property assigns as object[key] = value does and returns whether the assignment succeeded. Context::define_own_property defines a property from an OrdinaryPropertyDescriptor; OrdinaryPropertyDescriptor::data(value, writable, enumerable, configurable) builds a data descriptor.
let config = context.new_object()?;
let debug = runtime.intern_property_key("debug")?;
context.set_property(&config, &debug, Value::Bool(true))?;
// A read-only, non-enumerable property.
let version = runtime.intern_property_key("version")?;
let descriptor = OrdinaryPropertyDescriptor::data(Value::Int(3), false, false, false);
assert!(context.define_own_property(&config, &version, &descriptor)?);
let ports = context.new_array_from_values(vec![Value::Int(80), Value::Int(443)])?;
let ports_key = runtime.intern_property_key("ports")?;
context.set_property(&config, &ports_key, Value::Object(ports))?;
// Hand the object to JavaScript.
let stringify = context.eval("(value) => JSON.stringify(value)")?;
let stringify = runtime
.as_callable(stringify.as_object().expect("an object"))?
.expect("a function");
let json = context.call(&stringify, Value::Undefined, &[Value::Object(config)])?;
assert_eq!(
context.to_js_string(&json)?.to_utf8_lossy(),
r#"{"debug":true,"ports":[80,443]}"#
);Call functions
Context::call(&callable, this, &arguments) calls a function as callable.call(this, ...arguments) does. Pass Value::Undefined as this for a plain function call, or the object for a method call.
When the function throws, call returns Err(RuntimeError::Exception); Context::take_exception returns the thrown value and clears it.
let Value::Object(counter) =
context.eval("({ count: 10, add(n) { this.count += n; return this.count; } })")?
else {
panic!("expected an object");
};
let add_key = runtime.intern_property_key("add")?;
let Value::Object(add) = context.get_property(&counter, &add_key)? else {
panic!("expected a function");
};
let add = runtime.as_callable(&add)?.expect("add is callable");
// The second argument is `this`.
let result = context.call(&add, Value::Object(counter.clone()), &[Value::Int(5)])?;
assert_eq!(result, Value::Int(15));
// A function that throws returns RuntimeError::Exception.
let thrower = context.eval("(function () { throw new RangeError('too far'); })")?;
let thrower = runtime
.as_callable(thrower.as_object().expect("an object"))?
.expect("a function");
assert_eq!(
context.call(&thrower, Value::Undefined, &[]),
Err(RuntimeError::Exception)
);
let error = context.take_exception()?.expect("the thrown error");
assert_eq!(
context.to_js_string(&error)?.to_utf8_lossy(),
"RangeError: too far"
);Calls run synchronously to completion. A function that returns a promise returns the promise object; Promises and jobs shows how to run the jobs that settle it.
Construct objects
Context::construct(&constructor, &arguments) runs new constructor(...arguments). It throws a TypeError when the object is not a constructor. construct_with_new_target sets a different new.target, as Reflect.construct does.
let global = context.global_object()?;
let map_key = runtime.intern_property_key("Map")?;
let Value::Object(map_constructor) = context.get_property(&global, &map_key)? else {
panic!("expected Map");
};
let map_constructor = runtime
.as_callable(&map_constructor)?
.expect("Map is callable");
assert!(runtime.is_constructor(&map_constructor)?);
// new Map([["a", 1]])
let entry = context.new_array_from_values(vec![
Value::String(JsString::from_static("a")),
Value::Int(1),
])?;
let entries = context.new_array_from_values(vec![Value::Object(entry)])?;
let map = context.construct(&map_constructor, &[Value::Object(entries)])?;
assert_eq!(runtime.type_of(&map)?, "object");
let size_key = runtime.intern_property_key("size")?;
let map = map.as_object().expect("an object");
assert_eq!(context.get_property(map, &size_key)?, Value::Int(1));Convert values
Context::to_js_string and Context::to_number apply JavaScript's ToString and ToNumber. For objects they call Symbol.toPrimitive, valueOf or toString, so they run JavaScript and can throw. Value::to_boolean applies ToBoolean, which never runs JavaScript.
Runtime::print_value formats a value the way QuickJS prints it, without running any JavaScript. qjs uses it for uncaught exceptions.
// ToString and ToNumber run JavaScript: toString, valueOf, Symbol.toPrimitive.
let money = context.eval("({ valueOf() { return 42; }, toString() { return '$42'; } })")?;
assert_eq!(context.to_js_string(&money)?.to_utf8_lossy(), "$42");
assert_eq!(context.to_number(&money)?, 42.0);
assert!(
context
.to_number(&Value::String(JsString::from_static("abc")))?
.is_nan()
);
// typeof as the engine reports it (a function is "function").
let function = context.eval("Math.max")?;
assert_eq!(runtime.type_of(&function)?, "function");
// QuickJS's printer, as qjs prints an uncaught exception; it runs no JavaScript.
let value = context.eval("[1, 'two', { three: 3 }]")?;
assert_eq!(runtime.print_value(&value)?, "[ 1, \"two\", { three: 3 } ]");The global object
Context::global_object returns the realm's global object. Properties set on it from Rust are global variables in JavaScript, and global var and function declarations are readable from Rust.
let global = context.global_object()?;
// Rust sets a global; JavaScript reads it.
let limit = runtime.intern_property_key("LIMIT")?;
context.set_property(&global, &limit, Value::Int(100))?;
assert_eq!(context.eval("LIMIT * 2")?, Value::Int(200));
// JavaScript sets a global; Rust reads it.
context.eval("var status = 'done'")?;
let status = runtime.intern_property_key("status")?;
let value = context.get_property(&global, &status)?;
assert_eq!(context.to_js_string(&value)?.to_utf8_lossy(), "done");Other intrinsics have their own accessors: object_prototype, array_prototype, function_prototype, iterator_prototype, string_iterator_prototype, number_prototype, boolean_prototype, string_prototype, symbol_prototype, bigint_prototype and function_constructor.
JavaScript reaches the application through the Host trait: output, the clock, the time zone, the random seed, file reads and promise-rejection events. See Host services.