mirror of
https://github.com/lexmount/moli.git
synced 2026-11-02 00:06:25 +00:00
Add declarative V8 receiver access checks and Window brand validation. Authorize SameObject getters before cache reads, preserve captured objects for retired Realms, and materialize child proxies for the actual accessing context with isolated facade tokens.
326 lines
19 KiB
Rust
326 lines
19 KiB
Rust
//! Declarative Web API object and interface helpers.
|
|
//!
|
|
//! This crate is the Rust declaration layer that future WebIDL codegen can
|
|
//! target. It intentionally keeps conversion/parsing concerns in
|
|
//! `moli-webidl` and focuses on constructing V8 interface objects,
|
|
//! prototypes, and branded object instances.
|
|
//!
|
|
//! `#[derive(WebApiCrossOrigin)]` describes a stateless cross-origin property
|
|
//! table for native access-check handlers. Methods use ordinary V8 callbacks;
|
|
//! accessors use native value callbacks returning `Option`, with receiver
|
|
//! predicates checked before either invocation. Function templates are cached
|
|
//! per isolate and produce a unique function in each accessing context. The
|
|
//! table and templates never capture a target Window or an observer context.
|
|
//! Runtime V8 failures propagate to the script execution boundary.
|
|
//!
|
|
//! # Object declaration model
|
|
//!
|
|
//! A `#[derive(WebApiObject)]` struct describes the JavaScript surface that must
|
|
//! be installed on a V8 object. Field annotations decide whether a Rust field is
|
|
//! installed:
|
|
//!
|
|
//! - `#[webapi(data_property)]` defines a JavaScript own data property. It is
|
|
//! non-enumerable by default and can opt into `enumerable`, `readonly`, or
|
|
//! `dont_delete`. Rust slices, arrays, and vectors whose items implement
|
|
//! `WebApiValue` are converted into V8 arrays, so fixed-shape object records
|
|
//! can declare array-valued fields without hand-building `v8::Array`.
|
|
//! - `#[webapi(constant)]` defines a WebIDL constant own property. Constants
|
|
//! are enumerable, read-only, and non-configurable, and must provide
|
|
//! `value = ...`. The declaration installs the same descriptor shape used by
|
|
//! WebIDL constants on constructors and prototypes.
|
|
//! - `#[webapi(method)]` defines a JavaScript own function property. Its
|
|
//! `callback = ...` is the V8 callback, and optional `data = ...` is evaluated
|
|
//! while binding the object so callbacks can receive precomputed state.
|
|
//! Methods are non-enumerable, writable, and configurable by default; use
|
|
//! `enumerable`, `readonly`, or `dont_delete` when the target Web API requires
|
|
//! a different value descriptor.
|
|
//! `symbol = "iterator"`, `symbol = "asyncIterator"`, and
|
|
//! `symbol = "toStringTag"` use the matching well-known symbols as the
|
|
//! property key instead of deriving a string key from the Rust field name. A
|
|
//! method field typed as `Option<()>` is installed
|
|
//! only when it is `Some(())`; this is intended for runtime-selected fixed own
|
|
//! methods whose descriptors still belong to the declaration when present.
|
|
//! - `#[webapi(alias = "sourceName")]` defines an own data property whose value
|
|
//! is copied from another already-declared own property on the same object.
|
|
//! This is useful when Web APIs expose the same function through multiple
|
|
//! keys, such as a string-named method and a well-known symbol, and JavaScript
|
|
//! must observe the two properties as the exact same function object.
|
|
//! - `#[webapi(accessor_property)]` defines a JavaScript accessor property. Its
|
|
//! `getter = ...` and optional `setter = ...` are V8 function callbacks, and
|
|
//! optional `data = ...` follows the same callback-data model as declared
|
|
//! methods. Accessors with different setter callback data can use
|
|
//! `setter_data = ...`; otherwise the setter receives the same data as the
|
|
//! getter. Use `getter_value = ...` instead of `getter = ...` only when the
|
|
//! getter function object is already built or cached by surrounding runtime
|
|
//! code. Accessors also support the same `symbol = ...` keys when the
|
|
//! getter/setter pair represents a symbol-keyed Web API member.
|
|
//! - `#[webapi(native_data_property)]` defines a callback-backed V8 native data
|
|
//! property through `Object::set_native_data_property_with_configuration`.
|
|
//! Its `getter = ...` and optional `setter = ...` are V8 property callbacks,
|
|
//! not JavaScript function callbacks. Reserve this for surfaces whose
|
|
//! semantics intentionally operate on `PropertyCallbackArguments::holder()`.
|
|
//! - `#[webapi(hidden)]` defines a non-enumerable, string-named own property.
|
|
//! This is useful for legacy bookkeeping, but it is still an own property and
|
|
//! can be found by reflection that asks for all own property names.
|
|
//! - `#[webapi(slot)]` stores data in a V8 private slot through
|
|
//! `moli-v8-util`. Use this for internal state that must not appear as a
|
|
//! JavaScript own property.
|
|
//! - `#[webapi(prototype)]` and `#[webapi(to_string_tag)]` install runtime
|
|
//! prototype and `Symbol.toStringTag` metadata from field values.
|
|
//! `to_string_tag` fields are non-enumerable by default and can opt into
|
|
//! `readonly` or `dont_delete` descriptor bits.
|
|
//! - `#[webapi(init = ...)]` can initialize fixed default values without a
|
|
//! Rust value field: `null`, `undefined`, `object`, `null_object`, `array`,
|
|
//! `true`, `false`, `0`, `""`, or `string("...")`. Object-valued defaults
|
|
//! use named string forms such as `init = "array"`; primitive defaults use
|
|
//! literal or typed initializer syntax.
|
|
//!
|
|
//! Field names are converted to WebIDL-style camelCase by default. Use
|
|
//! struct-level `rename_all = "none"` only for declarations that intentionally
|
|
//! expose Rust field spelling. Struct-level `#[webapi(enumerable)]` makes
|
|
//! explicitly declared string-keyed data properties, methods, accessor
|
|
//! properties, native data properties, and aliases enumerable by default. It
|
|
//! can be used with `#[webapi(data_properties)]` for plain objects,
|
|
//! or by itself for WebIDL prototype declarations where every operation and
|
|
//! attribute should inherit WebIDL's enumerable prototype-member default.
|
|
//! Well-known symbol keys remain non-enumerable unless the field itself
|
|
//! declares `enumerable`.
|
|
//!
|
|
//! Fields without one of those installation annotations are declaration-only
|
|
//! inputs. The derive skips them completely, so they do not become properties or
|
|
//! private slots. They can still be referenced by method attributes such as
|
|
//! `data = self.some_field`, which lets a declaration carry callback data
|
|
//! without expanding the object reflection surface.
|
|
//!
|
|
//! Object declarations choose one role:
|
|
//!
|
|
//! - `#[webapi(interface = interfaces::Event)]` creates a native instance. It
|
|
//! receives private identity after its fields initialize successfully, whether
|
|
//! called through `bind`, `bind_into`, or `initialize`. Its default prototype
|
|
//! comes from the descriptor; an explicit `prototype` can preserve another
|
|
//! public shape without changing native identity.
|
|
//! - `#[webapi(plain)]` describes an ordinary unbranded object, defaulting to
|
|
//! `Object.prototype`. Field annotations and `data_properties` still control
|
|
//! which Rust fields become JavaScript properties.
|
|
//! - `#[webapi(fragment)]` installs shared state or members on an existing
|
|
//! target. It assigns no identity and has no implicit prototype. Explicit
|
|
//! prototype or tag declarations are applied by `bind_into`; `initialize`
|
|
//! installs only the declared fields. Fragments never erase native identity.
|
|
//!
|
|
//! Empty declarations need no special opt-in: creating an empty plain object,
|
|
//! branding a native instance and installing prototype metadata all have defined roles.
|
|
//! A single lifetime parameter is inferred as the V8 scope lifetime; use
|
|
//! `scope_lifetime` only when a declaration has multiple lifetimes. Payload slots
|
|
//! hold data, ownership and lifecycle state, not additional boolean brands.
|
|
//! Explicit prototype and tag expressions can use `Type::DESCRIPTOR.name()`
|
|
//! to share names with generated interface metadata.
|
|
//!
|
|
//! Implementation sharing does not imply interface inheritance. For example,
|
|
//! encoding streams share TransformStream internals but have distinct native
|
|
//! interfaces. Their factory assigns the concrete type while the shared state
|
|
//! declaration uses `fragment`. Do not turn actual native instances into plain
|
|
//! objects to make them pass structured clone.
|
|
//!
|
|
//! Declare each interface once with `declare_web_api_interfaces!` and reference
|
|
//! it using `#[webapi(interface = interfaces::ProgressEvent)]`. The generated
|
|
//! descriptor supplies the name and parent to constructor specs, factories and
|
|
//! receiver predicates. Initialization registers the full ancestry, including
|
|
//! when public constructors are absent. Do not repeat `parent` on object state
|
|
//! declarations. A template's optional `name` is only its display name and must
|
|
//! not be used to infer the identity of a shared implementation fragment.
|
|
//!
|
|
//! Identity is independent of realm exposure and mutable JavaScript prototypes.
|
|
//! Use `web_api_object_type` for the primary interface. Native factories can use
|
|
//! `Interface::DESCRIPTOR.initialize(scope, object)`; dynamic factories resolve
|
|
//! their selected name through the same native catalog. JavaScript subclasses
|
|
//! retain the interface implemented by their native constructor, and base
|
|
//! initialization never downgrades a derived object. Inconsistent parents,
|
|
//! cycles and unrelated rebranding fail.
|
|
//!
|
|
//! `WebApiFunctionTemplate` adapts native constructor
|
|
//! callbacks automatically. Hand-written templates can use
|
|
//! `web_api_constructor!(interfaces::Interface, callback)` when construction does not
|
|
//! already go through a named object declaration. Only successful construction
|
|
//! brands the resulting native object; constructors and prototypes are not
|
|
//! instances. A factory implemented with a native Proxy must brand its target
|
|
//! and call `register_web_api_proxy` with its private handler before publication.
|
|
//! The handler belongs to that exact Proxy; author wrappers and revoked Proxies
|
|
//! fail identity checks without invoking traps. Registration grants no clone
|
|
//! capability. Native factories select names from their own descriptors, never
|
|
//! from JavaScript properties such as `new.target.name`. Include detached nodes,
|
|
//! workers, iterators, fallback events, and deserialization paths when adding a
|
|
//! factory. Receiver identity does not replace resource or realm authorization.
|
|
//!
|
|
//! Identity alone grants neither serialization nor transfer capability.
|
|
//! Structured-clone serializers route every identified object through their
|
|
//! explicit codecs and reject unsupported types, including nested instances.
|
|
//! Both message serialization and IndexedDB dispatch on the primary interface,
|
|
//! as required by the [HTML serialization and transfer rules]. A native derived
|
|
//! interface does not implicitly inherit a base interface's codec. The numeric
|
|
//! type IDs are isolate-local and must never be persisted or used as wire tags;
|
|
//! deserializers use their existing wire tags and native construction paths to
|
|
//! assign identity in the receiving isolate.
|
|
//!
|
|
//! [HTML serialization and transfer rules]: https://html.spec.whatwg.org/multipage/structured-data.html#serializable-objects
|
|
//!
|
|
//! The derive generates a Rust-side `new(...)` constructor by default. The
|
|
//! generated constructor takes every non-`()` declaration field as a named
|
|
//! argument and fills `()` declaration fields with `()`. If every field is
|
|
//! `()`, the generated constructor is `new()`. This keeps dynamic state
|
|
//! explicit while removing boilerplate for fixed initialized slots or
|
|
//! accessor/method declaration fields. A field can declare
|
|
//! `#[webapi(constructor_default = expr)]`, or bare
|
|
//! `#[webapi(constructor_default)]` for `Default::default()`, to keep a
|
|
//! Rust-side default out of the generated constructor while still installing
|
|
//! the field normally; this is distinct from `init = ...`, which creates a
|
|
//! JavaScript/V8-side default for a `()` field. Use
|
|
//! `#[webapi(no_dynamic_constructor)]` when the declaration already has a
|
|
//! hand-written constructor with narrower semantics.
|
|
//!
|
|
//! # Receiver checks and Promise-returning members
|
|
//!
|
|
//! `#[webapi(interface = interfaces::Event, receiver)]` checks instance methods
|
|
//! and accessors against the declared interface before argument conversion.
|
|
//! A field can use `receiver = interfaces::Other::is_instance` to select another
|
|
//! interface. Merely naming a template does not enable receiver checks.
|
|
//! Invalid receivers throw `TypeError("Illegal invocation")`. Native subtypes
|
|
//! and cross-realm objects are accepted even after their JavaScript prototypes
|
|
//! change. Forged prototypes, public properties, and author Proxies cannot pass.
|
|
//!
|
|
//! Struct-level `receiver` supplies the default for instance methods and
|
|
//! accessor properties in both derives; a field can override it. Static
|
|
//! methods, data properties, and holder-based native data properties do not
|
|
//! inherit this policy. Already-built `getter_value` functions cannot use it.
|
|
//!
|
|
//! `receiver = path` remains available for a custom native predicate with
|
|
//! signature `fn(&mut v8::PinScope, v8::Local<v8::Object>) -> bool`. It must not
|
|
//! execute JavaScript or throw. Use it for checks that additionally require
|
|
//! native resource state; ordinary interface checks should use their descriptor.
|
|
//!
|
|
//! `receiver_access_check` enables V8's receiver access check before invoking
|
|
//! native methods and accessor callbacks. Window bindings use it to reject
|
|
//! cross-origin borrowed calls before argument conversion or a realm switch.
|
|
//! It can be a struct default or a member policy. A member can use `false` to
|
|
//! allow cross-origin calls, or `"getter"` / `"setter"` to check only that half
|
|
//! of an accessor. Interface brand checks remain a separate policy.
|
|
//!
|
|
//! `returns_promise` on a method (including a static method) or accessor getter
|
|
//! converts synchronous exceptions from both the receiver check and the callback
|
|
//! into rejected Promises in the callback's realm. Successful return values are
|
|
//! unchanged, so cached Promise identity is preserved. An accessor setter still
|
|
//! throws synchronously. Callback data and native function descriptors are
|
|
//! unchanged: these adapters are Rust callbacks, not JavaScript wrappers.
|
|
//!
|
|
//! # Function-template declaration model
|
|
//!
|
|
//! A `#[derive(WebApiFunctionTemplate)]` struct describes a constructor-backed
|
|
//! V8 `FunctionTemplate`, its constructor-template static methods, and its
|
|
//! prototype-template methods. This is the declaration path for bootstrap
|
|
//! surfaces that install onto `FunctionTemplate` and
|
|
//! `FunctionTemplate::prototype_template()` instead of an already-created V8
|
|
//! object. Template methods are non-enumerable by default, and can opt into
|
|
//! `enumerable`, `readonly`, or `dont_delete`; struct-level
|
|
//! `#[webapi(enumerable)]` applies the WebIDL operation default to string-keyed
|
|
//! methods while leaving well-known symbols explicit. Template
|
|
//! `#[webapi(accessor_property)]` creates getter/setter `FunctionTemplate`
|
|
//! values through `ObjectTemplate::set_accessor_property`, so callbacks receive
|
|
//! the actual JavaScript receiver through `FunctionCallbackArguments::this()`.
|
|
//! `#[webapi(native_data_property)]` is the explicit spelling for the
|
|
//! holder-based `ObjectTemplate::set_native_data_property_with_configuration`
|
|
//! path and should be reserved for internal properties that intentionally
|
|
//! operate on the native holder.
|
|
//! `#[webapi(intrinsic_data_property = v8::Intrinsic::...)]` installs a
|
|
//! realm-correct V8 intrinsic directly on the prototype template. This
|
|
//! template-only path avoids reading mutable public builtins such as
|
|
//! `globalThis.Array.prototype` and supports the normal descriptor flags and
|
|
//! well-known symbol keys.
|
|
//! Struct-level
|
|
//! `#[webapi(intrinsic_prototype_parent = v8::Intrinsic::...)]` creates the
|
|
//! hidden parent template required by V8 and links the declared prototype to a
|
|
//! realm-correct intrinsic prototype. This is intended for WebIDL iterator
|
|
//! prototypes and special interfaces such as `DOMException`; it does not read
|
|
//! public constructors such as `globalThis.Error`.
|
|
//! WebIDL iterator templates can pair this with struct-level
|
|
//! `readonly_prototype` and `prototype_to_string_tag = "..."`; the latter
|
|
//! installs the read-only, non-enumerable `Symbol.toStringTag` required by the
|
|
//! iterator prototype algorithms.
|
|
//! `#[webapi(constant)]` installs WebIDL constants on both the constructor
|
|
//! template and prototype template.
|
|
//!
|
|
//! Global interface exposure is deliberately outside either derive. The
|
|
//! renderer's aggregate exposed-interface installer uses V8 lazy data
|
|
//! properties so one complete interface—not an individual declaration
|
|
//! fragment—owns first-read materialization.
|
|
|
|
extern crate self as moli_webapi_declare;
|
|
|
|
mod brand;
|
|
mod callback;
|
|
mod cross_origin;
|
|
mod declaration;
|
|
mod error;
|
|
mod interface;
|
|
mod property;
|
|
mod prototype;
|
|
mod value;
|
|
|
|
pub mod __private;
|
|
|
|
pub use interface::WebApiInterfaceDescriptor;
|
|
pub use moli_webapi_declare_derive::{WebApiCrossOrigin, WebApiFunctionTemplate, WebApiObject};
|
|
pub use v8;
|
|
|
|
/// Adapts a manual native constructor to the same identity initialization used
|
|
/// by interface derives. The interface must be a generated native identity type,
|
|
/// never a name obtained from `new.target` or another JavaScript property.
|
|
#[macro_export]
|
|
macro_rules! web_api_constructor {
|
|
($interface:path, $callback:path) => {{
|
|
fn __moli_native_constructor_adapter<'s>(
|
|
scope: &mut $crate::v8::PinScope<'s, '_>,
|
|
args: $crate::v8::FunctionCallbackArguments<'s>,
|
|
rv: $crate::v8::ReturnValue<'s>,
|
|
) {
|
|
$crate::__private::invoke_web_api_constructor(
|
|
scope,
|
|
args,
|
|
rv,
|
|
<$interface>::DESCRIPTOR,
|
|
$callback,
|
|
);
|
|
}
|
|
__moli_native_constructor_adapter
|
|
}};
|
|
}
|
|
|
|
pub use brand::{
|
|
WebApiType, implements_interface, initialize_web_api_object, register_web_api_interfaces,
|
|
register_web_api_proxy, web_api_object_target, web_api_object_type,
|
|
};
|
|
|
|
pub use cross_origin::{
|
|
CrossOriginFunction, CrossOriginGetter, CrossOriginProperty, CrossOriginSetter,
|
|
WebApiCrossOriginDeclaration,
|
|
};
|
|
pub use declaration::{
|
|
DataPropertyDescriptorDeclaration, ObjectLiteralDeclaration, WebApiFunctionTemplateDeclaration,
|
|
WebApiObjectDeclaration,
|
|
};
|
|
pub use error::BindError;
|
|
pub use property::{
|
|
define_bool_property, define_declared_accessor_property,
|
|
define_declared_accessor_property_by_key, define_declared_constant_property,
|
|
define_declared_data_property, define_declared_data_property_with_attributes,
|
|
define_declared_enumerable_data_property, define_declared_hidden_property,
|
|
define_declared_hidden_property_with_descriptor, define_declared_private_slot,
|
|
define_enumerable_value_property, define_hidden_property, define_number_property,
|
|
define_private_slot, define_string_property, define_value_property,
|
|
illegal_constructor_callback, webidl_constant_property_attributes,
|
|
};
|
|
pub use prototype::{
|
|
define_declared_to_string_tag, define_declared_to_string_tag_with_attributes,
|
|
define_to_string_tag, define_to_string_tag_with_attributes, set_declared_prototype,
|
|
set_interface_prototype, set_required_interface_prototype,
|
|
};
|
|
pub use value::{WebApiTemplateValue, WebApiValue, define_array_data_property};
|