libs/ops/op2/README.md
#[op2] is the in-progress replacement for #[op].
Strings in Rust are always UTF-8. Strings in v8, however, are either
two-byte UTF-16 or one-byte Latin-1. One-byte Latin-1 strings are not
byte-compatible with UTF-8, as characters with the index 128-255 require two
bytes to encode in UTF-8.
Because of this, Strings in ops always require a copy (at least) to ensure
that we are not incorrectly passing Latin-1 data to methods that expect a UTF-8
string. At this time there is no way to avoid this copy, though the op code
does attempt to avoid any allocations where possible by making use of a stack
buffer.
opsAn op function may be declared to return Result to indicate that the op is
fallible. The error type must implement deno_error::JsErrorClass. When the
function returns Err, an exception is thrown.
async callsAsynchronous calls are fully inferred from the function definition. Asynchronous calls are supported in two forms:
async fn op_xyz(/* ... */) -> X {}
and
fn op_xyz(/* ... */) -> impl Future<Output = X> {}
These are desugared to a function that adds a hidden promise_id argument, and
returns Option<X> instead. Deno will eagerly poll the op, and if it is
immediately ready, the function will return Some(X). If the op is not ready,
the function will return None and the future will be handled by Deno's pending
op system.
fn op_xyz(promise_id: i32 /* ... */) -> Option<X> {}
async callsBy default, async functions are eagerly polled, which reduces the latency of
the call dramatically if the async function is ready to return a value
immediately.
async(lazy)async calls may be marked as lazy, which allows the runtime to defer polling
the op until a later time. The submission of an async(lazy) op might be
faster, but the latency will be higher for ops that would have been ready on the
first poll.
NOTE: You may need to use this to get the maximum performance out of a set of async tasks, but it should only be used alongside careful benchmarking. In some cases it will allow for higher throughput at the expense of latency.
Lazy async calls may be fastcalls, though the resolution will still happen
on a slow path.
async(deferred)async calls may also be marked as deferred, which will allow the runtime to
poll the op immediately, but any results that are ready are deferred until a
later run of the event loop.
NOTE: This is almost certainly not what you want to use and should only be used if you really know what you are doing.
Lazy async(deferred) calls may be fastcalls, though the resolution will
still happen on a slow path.
op2 requires fastcall-compatible ops to be annotated with fast. If you wish
to avoid fastcalls for some reason (this is unlikely), you can specify nofast
instead.
You may also choose an alternate op function to use as the fastcall equivalent
to a slow function. In this case, you can specify fast(op_XYZ). The other op
must be decorated with #[op2(fast)], and does not need to be registered. When
v8 optimized the slow function to a fastcall, it will switch the implementation
over if the parameters are compatible. This is useful for a function that takes
any buffer type in the slow path and wishes to use the very fast typed u8
buffer for the fast path.
Arguments in non-fast ops use the deno_core::convert::FromV8Scopeless trait by
default. This trait does not require a v8 scope for conversion, making it more
efficient for many types.
To use the FromV8 trait instead (which provides access to a v8 scope during
conversion), add the #[scoped] attribute to the argument:
fn op_xyz(#[scoped] arg: MyFromV8Type) -> X {}
Return types use the ToV8 trait by default in non-fast ops. Any type that
implements deno_core::convert::ToV8 can be returned directly without any
attribute.
op2 supports defining native JavaScript classes backed by Rust types using
V8's CppGC (C++ garbage collector). These objects live on the V8 heap and are
automatically garbage collected.
Define a struct that implements GarbageCollected, then use #[op2] on an
impl block to define its JavaScript API:
use deno_core::GarbageCollected;
use deno_core::v8::cppgc::GcCell;
#[repr(C)]
pub struct MyObject {
value: GcCell<f64>,
}
unsafe impl GarbageCollected for MyObject {
fn trace(&self, _visitor: &mut v8::cppgc::Visitor) {}
fn get_name(&self) -> &'static std::ffi::CStr {
c"MyObject"
}
}
#[op2]
impl MyObject {
#[constructor]
#[cppgc]
fn new(value: f64) -> MyObject {
MyObject {
value: GcCell::new(value),
}
}
#[getter]
fn value(&self, isolate: &v8::Isolate) -> f64 {
*self.value.get(isolate)
}
#[setter]
fn value(&self, isolate: &mut v8::Isolate, value: f64) {
self.value.set(isolate, value);
}
#[fast]
fn double_value(&self, isolate: &v8::Isolate) -> f64 {
*self.value.get(isolate) * 2.0
}
#[static_method]
#[cppgc]
fn create(value: f64) -> MyObject {
MyObject {
value: GcCell::new(value),
}
}
}
Register the object in your extension:
deno_core::extension!(
my_ext,
objects = [MyObject],
// ...
);
The object is then available in JavaScript:
import { MyObject } from "ext:core/ops";
const obj = new MyObject(42);
console.log(obj.value); // 42
console.log(obj.doubleValue()); // 84
obj.value = 10;
#[constructor] — The JS constructor. Must return the struct type (optionally
wrapped in Result). Mark with #[cppgc] to indicate the return is a CppGC
object.#[getter] / #[setter] — Property accessors. Getter and setter for the same
property should share the same function name.#[static_method] — A static method on the class (e.g., MyObject.create()).#[fast] — Regular methods. Use &self as the first parameter to receive the
native object.CppGC objects support a prototype-based inheritance model that mirrors
JavaScript's class inheritance. This allows you to define a base class in Rust
and have derived classes inherit its methods and properties, just like
class Child extends Parent in JavaScript.
Mark the struct with #[derive(CppgcBase)] and its impl block with
#[op2(base)]:
use deno_core::CppgcBase;
#[derive(CppgcBase)]
#[repr(C)]
pub struct Shape {
sides: GcCell<u32>,
}
unsafe impl GarbageCollected for Shape {
fn trace(&self, _visitor: &mut v8::cppgc::Visitor) {}
fn get_name(&self) -> &'static std::ffi::CStr {
c"Shape"
}
}
#[op2(base)]
impl Shape {
#[constructor]
#[cppgc]
fn new(sides: u32) -> Shape {
Shape {
sides: GcCell::new(sides),
}
}
#[getter]
fn sides(&self, isolate: &v8::Isolate) -> u32 {
*self.sides.get(isolate)
}
}
The base attribute tells op2 to use a polymorphic unwrap when accessing
&self, so that methods on Shape can be called on any type that inherits from
it.
Mark the struct with #[derive(CppgcInherits)], put the base type as the first
field, and use #[op2(inherit = BaseType)] on the impl block:
use deno_core::CppgcInherits;
#[derive(CppgcInherits)]
#[cppgc_inherits_from(Shape)]
#[repr(C)]
pub struct Rectangle {
base: Shape, // must be the first field
width: GcCell<f64>,
height: GcCell<f64>,
}
unsafe impl GarbageCollected for Rectangle {
fn trace(&self, _visitor: &mut v8::cppgc::Visitor) {}
fn get_name(&self) -> &'static std::ffi::CStr {
c"Rectangle"
}
}
#[op2(inherit = Shape)]
impl Rectangle {
#[constructor]
#[cppgc]
fn new(width: f64, height: f64) -> Rectangle {
Rectangle {
base: Shape {
sides: GcCell::new(4),
},
width: GcCell::new(width),
height: GcCell::new(height),
}
}
#[fast]
fn area(&self, isolate: &v8::Isolate) -> f64 {
*self.width.get(isolate) * *self.height.get(isolate)
}
}
In JavaScript, Rectangle inherits from Shape:
const rect = new Rectangle(3, 4);
console.log(rect.sides); // 4 (inherited from Shape)
console.log(rect.area()); // 12
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Shape); // true
JavaScript classes can also extend these native classes:
class Square extends Rectangle {
constructor(size) {
super(size, size);
}
}
const sq = new Square(5);
console.log(sq.area()); // 25
console.log(sq.sides); // 4
If a derived class will itself be inherited from, it must also be a base class.
Derive both CppgcInherits and CppgcBase, and use
#[op2(base, inherit = ParentType)]:
// Rectangle is both a child of Shape AND a base for further derivation.
#[derive(CppgcInherits, CppgcBase)]
#[cppgc_inherits_from(Shape)]
#[repr(C)]
pub struct Rectangle {
base: Shape,
width: GcCell<f64>,
height: GcCell<f64>,
}
#[op2(base, inherit = Shape)]
impl Rectangle {
// ... methods ...
}
// Square inherits from Rectangle
#[derive(CppgcInherits)]
#[cppgc_inherits_from(Rectangle)]
#[repr(C)]
pub struct Square {
base: Rectangle,
}
#[op2(inherit = Rectangle)]
impl Square {
// ... methods ...
}
#[repr(C)].#[derive(CppgcInherits)].#[derive(CppgcBase)].#[derive(CppgcInherits, CppgcBase)].objects = [...] list, with
base types listed before their derived types.bool
i8
u8
i16
u16
i32
u32
#[smi] ResourceId
#[bigint] i64
#[bigint] u64
#[bigint] isize
#[bigint] usize
f32
f64
#[string] String
#[string] &str
#[string] Cow<str>
#[string(onebyte)] Cow<[u8]>
&v8::Value
&v8::String
&v8::Object
&v8::Function
&v8::...
v8::Local<v8::Value>
v8::Local<v8::String>
v8::Local<v8::Object>
v8::Local<v8::Function>
v8::Local<v8::...>
FromV8Scopeless
#[scoped] FromV8Type
#[scoped] (Tuple, Tuple)
#[serde] SerdeType
#[serde] (Tuple, Tuple)
#[arraybuffer] &mut [u8]
#[arraybuffer] &[u8]
#[arraybuffer] *mut u8
#[arraybuffer] *const u8
#[arraybuffer(copy)] Vec<u8>
#[arraybuffer(copy)] Box<[u8]>
#[arraybuffer(copy)] bytes::Bytes
#[buffer(copy)] Vec<u8>
#[buffer(copy)] Box<[u8]>
#[buffer(copy)] bytes::Bytes
#[buffer] &mut [u32]
#[buffer] &[u32]
#[buffer(copy)] Vec<u32>
#[buffer(copy)] Box<[u32]>
#[buffer(detach)] JsBuffer
*const std::ffi::c_void
*mut std::ffi::c_void
&OpState
&mut OpState
Rc<RefCell<OpState>>
&JsRuntimeState
bool
i8
u8
i16
u16
i32
u32
#[smi] ResourceId
#[bigint] i64
#[bigint] u64
#[bigint] isize
#[bigint] usize
#[number] i64
#[number] u64
#[number] isize
#[number] usize
f32
f64
#[string] String
#[string] &str
#[string] Cow<str>
#[string(onebyte)] Cow<[u8]>
#[arraybuffer] V8Slice<u8>
#[arraybuffer] Vec<u8>
#[arraybuffer] Box<[u8]>
#[arraybuffer] bytes::BytesMut
#[buffer] V8Slice<u8>
#[buffer] Vec<u8>
#[buffer] Box<[u8]>
#[buffer] bytes::BytesMut
#[buffer] V8Slice<u32>
*const std::ffi::c_void
*mut std::ffi::c_void
v8::Local<v8::Value>
v8::Local<v8::String>
v8::Local<v8::Object>
v8::Local<v8::Function>
v8::Local<v8::...>
ToV8Type
(ToV8Type, ToV8Type)
#[serde] SerdeType
#[serde] (SerdeType, SerdeType)