Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/reference/rust-review-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ Unsafe code is concentrated at FFI boundaries. Review every `unsafe` block and `
- **Always** ensure that library crates (e.g., `dns`, `net`, `transpiler`) use `thiserror` for
domain-specific error types. These should implement `Display` and have descriptive error messages.
- **Always** ensure that JSG-facing crates (e.g., `api`, `jsg`) use `jsg::Error` with an
`ExceptionType` variant (e.g., `TypeError`, `RangeError`). Domain errors should implement
`From<DomainError> for jsg::Error` for ergonomic `?` usage.
`ExceptionType` variant. `TypeError`, `RangeError` and the other unprefixed variants throw
the JS built-in error; a `DOM`-prefixed variant throws a `DOMException` with the unprefixed
name (`DOMSyntaxError` throws `DOMException` named `SyntaxError`), as in C++ `JSG_REQUIRE`.
Domain errors should implement `From<DomainError> for jsg::Error` for ergonomic `?` usage.
- **Never** use `panic!` for expected errors. Panics across FFI are undefined behavior. Use
`Result` and propagate errors through the CXX bridge. The build denies `unwrap()`, `expect()`,
`panic!`, `todo!` and `unimplemented!` in production code. `unreachable!` is allowed: a panic is
Expand Down
6 changes: 6 additions & 0 deletions src/per_isolate/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ at context creation, before any user code. C++ entry point:
`view-extent`).
- TypeScript `import type` / `export type` are used freely for type
plumbing (fully erased; the loader only sees `module.exports`).
- `main.ts` first installs the TypeScript `DOMException` (`dom-exception.ts`),
which replaces the C++ class with one of the same shape. Native code creates
instances of it without running its constructor, and recognizes one by the
brand in its V8 private properties (`newDOMException()` and
`tryGetDOMExceptionFields()` in `src/workerd/jsg/jsg.h`); bootstrap code checks
for one with `utils.isDOMException()`, never `instanceof`.
- `main.ts` installs the real stream globals (ReadableStream et al.) when
the `typescript_implemented_streams` compat flag is enabled, plus the
internal-testing `ReadableStreamDrainingReader` under
Expand Down
152 changes: 152 additions & 0 deletions src/per_isolate/dom-exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
'use strict';

// The global DOMException (https://webidl.spec.whatwg.org/#idl-DOMException).
//
// A DOMException looks as the C++ JSG class's instances do (src/workerd/jsg/dom-exception.h):
// own read-only `code`, `name` and `message` properties, an enumerable own `stack`, and the
// legacy code constants on both the constructor and the prototype. Native code creates
// DOMExceptions without running this constructor (jsg::newDOMException()), over the prototype
// that install() hands it, and recognizes them by the brand that utils.initDOMException() sets.
// This constructor sets the same brand, so utils.isDOMException() is true for either kind, and
// for a subclass instance.
//
// DOMException is a function rather than a class so that its `prototype` property is
// writable, as a JSG class's is unless the immutable_api_prototypes flag is set.

const {
ErrorCaptureStackTrace,
ErrorPrototype,
ObjectCreate,
ObjectDefineProperty,
ObjectFreeze,
ObjectGetOwnPropertyDescriptor,
ObjectGetPrototypeOf,
SymbolFor,
SymbolToStringTag,
TypeError,
} = primordials;

// The legacy codes, in the order JSG defines the constants
// (JSG_DOM_EXCEPTION_FOR_EACH_ERROR_NAME).
const LEGACY_CODES: readonly (readonly [string, number])[] = [
['INDEX_SIZE_ERR', 1],
['DOMSTRING_SIZE_ERR', 2],
['HIERARCHY_REQUEST_ERR', 3],
['WRONG_DOCUMENT_ERR', 4],
['INVALID_CHARACTER_ERR', 5],
['NO_DATA_ALLOWED_ERR', 6],
['NO_MODIFICATION_ALLOWED_ERR', 7],
['NOT_FOUND_ERR', 8],
['NOT_SUPPORTED_ERR', 9],
['INUSE_ATTRIBUTE_ERR', 10],
['INVALID_STATE_ERR', 11],
['SYNTAX_ERR', 12],
['INVALID_MODIFICATION_ERR', 13],
['NAMESPACE_ERR', 14],
['INVALID_ACCESS_ERR', 15],
['VALIDATION_ERR', 16],
['TYPE_MISMATCH_ERR', 17],
['SECURITY_ERR', 18],
['NETWORK_ERR', 19],
['ABORT_ERR', 20],
['URL_MISMATCH_ERR', 21],
['QUOTA_EXCEEDED_ERR', 22],
['TIMEOUT_ERR', 23],
['INVALID_NODE_TYPE_ERR', 24],
['DATA_CLONE_ERR', 25],
];

// The message and name are converted as Web IDL converts a DOMString, which a template literal
// does too: a Symbol throws a TypeError.
function DOMException(this: object, ...args: unknown[]): void {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you mentioned that you're still iterating.. but I'm curious... why a function and not a class DOMException?

if (new.target === undefined) {
throw new TypeError(
"Failed to construct 'DOMException': Please use the 'new' operator, this object constructor cannot be called as a function."
);
}
const message = args[0] === undefined ? '' : `${args[0] as string}`;
const name = args[1] === undefined ? 'Error' : `${args[1] as string}`;
utils.initDOMException(this, name, message);
// Leaves out this constructor's frame, but not a subclass constructor's.
ErrorCaptureStackTrace(this, DOMException);
// Error properties are not enumerable, but a DOMException's always have been.
ObjectDefineProperty(this, 'stack', { __proto__: null, enumerable: true });
}

// Defines `key` on `target` with the attributes it has on `source`, which JSG chooses by
// compatibility flags, and `value`.
function copyProperty(
target: object,
source: object,
key: string | symbol,
value: unknown
): void {
const descriptor = ObjectGetOwnPropertyDescriptor(source, key);
if (descriptor === undefined) {
throw new TypeError(`DOMException has no ${String(key)} to copy`);
}
ObjectDefineProperty(target, key, { ...descriptor, __proto__: null, value });
}

// Gives DOMException the properties of `native`, the C++ class it replaces, with their
// attributes and in their order.
function setUpClass(native: Function): void {
const nativePrototype = native.prototype as object;
copyProperty(DOMException, native, 'length', 0);
copyProperty(DOMException, native, 'name', 'DOMException');

const prototype = ObjectCreate(ErrorPrototype) as object;
for (let i = 0; i < LEGACY_CODES.length; i++) {
const [name, code] = LEGACY_CODES[i] as readonly [string, number];
copyProperty(DOMException, native, name, code);
copyProperty(prototype, nativePrototype, name, code);
}
copyProperty(prototype, nativePrototype, 'constructor', DOMException);
// Present under the set_tostring_tag flag.
if (
ObjectGetOwnPropertyDescriptor(nativePrototype, SymbolToStringTag) !==
undefined
) {
copyProperty(prototype, nativePrototype, SymbolToStringTag, 'DOMException');
}
// Tools recognize a runtime class by this symbol.
const internalClass = SymbolFor('cloudflare:internal-class');
copyProperty(prototype, nativePrototype, internalClass, internalClass);
// util.inspect() formats an instance of a runtime class through this symbol.
const inspect = utils.getApiSymbol('kResourceTypeInspect');
copyProperty(
prototype,
nativePrototype,
inspect,
ObjectFreeze({ __proto__: null })
);

// Not writable under the immutable_api_prototypes flag.
copyProperty(DOMException, native, 'prototype', prototype);
}

// Replaces the C++ class as the global DOMException, keeping the property where JSG defines it
// (on the global object's prototype) and its attributes, and has native code create instances
// of this class from now on.
function install(): void {
let owner: object | null = globalThis;
while (owner !== null) {
const descriptor = ObjectGetOwnPropertyDescriptor(owner, 'DOMException');
if (descriptor !== undefined) {
setUpClass(descriptor.value as Function);
ObjectDefineProperty(owner, 'DOMException', {
...descriptor,
__proto__: null,
value: DOMException,
});
utils.setDOMExceptionPrototype(DOMException.prototype as object);
return;
}
owner = ObjectGetPrototypeOf(owner) as object | null;
}
throw new TypeError('the global object has no DOMException');
}

module.exports = {
install,
};
3 changes: 3 additions & 0 deletions src/per_isolate/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
//
const { ObjectDefineProperties } = primordials;

// Native code creates DOMExceptions of the class installed here, so it is installed first.
require('dom-exception').install();

if (compatFlags['typescript_implemented_streams']) {
const {
ReadableStream,
Expand Down
7 changes: 7 additions & 0 deletions src/per_isolate/per_isolate-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ declare const utils: {
// surrogate counts the 3 bytes of U+FFFD), without encoding it. Throws a
// TypeError for a non-string.
utf8Length(str: string): number;
// Gives `object`, a DOMException being constructed, its brand and its own `code`, `name` and
// `message` properties (jsg::initDOMException()). Consumed by dom-exception.
initDOMException(object: object, name: string, message: string): void;
// Whether `value` is a DOMException, by its brand. Runs no JavaScript.
isDOMException(value: unknown): boolean;
// Makes `prototype` the prototype of the DOMExceptions native code creates.
setDOMExceptionPrototype(prototype: object): void;
};

// An abort algorithm registration. Obtained only from
Expand Down
17 changes: 13 additions & 4 deletions src/per_isolate/primordials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ const WeakSetCtor = WeakSet;
// Function
const FunctionPrototypeBind = uncurryThis(FunctionPrototype.bind);

// Error
const ErrorPrototype = ErrorCtor.prototype;
// V8's Error.captureStackTrace(), which the standard library types leave out.
const ErrorCaptureStackTrace = (
ErrorCtor as unknown as {
captureStackTrace(target: object, constructorOpt?: Function): void;
}
).captureStackTrace;

// Object
const ObjectCreate = Object.create;
const ObjectDefineProperty = Object.defineProperty;
Expand Down Expand Up @@ -635,7 +644,6 @@ const EventTargetRemoveEventListener = uncurryThis(

const TextDecoderCtor = globalThis.TextDecoder;
const TextEncoderCtor = globalThis.TextEncoder;
const DOMException = globalThis.DOMException;

const TextEncoderEncode = uncurryThis(TextEncoderCtor.prototype.encode) as (
encoder: TextEncoder,
Expand Down Expand Up @@ -700,6 +708,10 @@ module.exports = ObjectFreeze({
FunctionPrototype,
FunctionPrototypeBind,

// Error
ErrorPrototype,
ErrorCaptureStackTrace,

// Object
ObjectPrototype,
ObjectCreate,
Expand Down Expand Up @@ -738,9 +750,6 @@ module.exports = ObjectFreeze({
ArrayPrototypeSlice,
ArrayPrototypeSplice,

// DOMException
DOMException,

// Map
MapPrototypeGet,
MapPrototypeSet,
Expand Down
4 changes: 2 additions & 2 deletions src/per_isolate/webfs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ Parts of the contract that are easy to break:
a method body runs, so a bad option bag is a synchronous throw while a file
that cannot be opened is a rejection. `createWritable` is therefore
deliberately **not** an `async function` — that would turn the former into a
rejection too. It classifies by exception type: a DOMException becomes a
rejection, anything else is rethrown. `badOptionsThrowSynchronously` and
rejection too. It classifies by exception type: a DOMException (by its brand,
`utils.isDOMException()`) becomes a rejection, anything else is rethrown. `badOptionsThrowSynchronously` and
`createWritableRejectsForeignReceiver` hold the two apart.
- **The option bag is coerced, not type-checked.** JSG unwraps
`jsg::Optional<bool>` with ToBoolean, so `{keepExistingData: 'false'}` opts
Expand Down
5 changes: 2 additions & 3 deletions src/per_isolate/webfs/writable-file-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,14 @@ import type {
} from '../webstreams/types';

const {
DOMException: NativeDOMException,
ObjectDefineProperties,
PromiseReject,
PromiseResolve,
SymbolToStringTag,
TypeError,
} = primordials;

const { createFileSystemWriteContext } = utils;
const { createFileSystemWriteContext, isDOMException } = utils;

const {
WritableStream,
Expand Down Expand Up @@ -196,7 +195,7 @@ function createWritable(
// A DOMException means the file could not be opened, which createWritable
// reports by rejecting. Anything else is the receiver check failing, which
// JSG performs before the method body runs and so throws.
if (err instanceof NativeDOMException) {
if (isDOMException(err)) {
return PromiseReject(err);
}
throw err;
Expand Down
5 changes: 5 additions & 0 deletions src/rust/jsg-test/ffi.c++
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ bool isolates_share_group(Isolate* a, Isolate* b) {
return a->GetGroup() == b->GetGroup();
}

void throw_kj_exception(::rust::Str description) {
kj::throwFatalException(kj::Exception(kj::Exception::Type::FAILED, __FILE__, __LINE__,
kj::heapString(description.data(), description.size())));
}

void hold_across_gc(Isolate* isolate,
::rust::Fn<::workerd::rust::jsg::MaybeLocal(Isolate*)> create,
::rust::Fn<void(Isolate*, ::workerd::rust::jsg::Local)> check) {
Expand Down
3 changes: 3 additions & 0 deletions src/rust/jsg-test/ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ ::workerd::rust::jsg::Local cpp_deserialize(Isolate* isolate, ::rust::Slice<cons
// Returns whether `a` and `b` belong to the same v8::IsolateGroup.
bool isolates_share_group(Isolate* a, Isolate* b);

// Throws a kj::Exception with `description`.
void throw_kj_exception(::rust::Str description);

// Holds a value the way C++ code holds one that Rust created: calls `create`, keeps its result
// only in a v8::Global while a full GC runs, and calls `check` with it. Then resets the Global
// and runs a full GC again. Neither call's handles outlive the call.
Expand Down
6 changes: 6 additions & 0 deletions src/rust/jsg-test/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ mod ffi {
/// Returns whether `a` and `b` belong to the same `v8::IsolateGroup`.
pub unsafe fn isolates_share_group(a: *mut Isolate, b: *mut Isolate) -> bool;

/// Throws a `kj::Exception` with `description`, such as a tunneled
/// `jsg-internal.DOMException(OperationError): ...`.
#[expect(clippy::allow_attributes)]
#[allow(dead_code)]
pub fn throw_kj_exception(description: &str) -> Result<()>;

/// Holds a value the way C++ code holds one that Rust created: calls `create`, keeps
/// its result only in a `v8::Global` while a full GC runs, and calls `check` with it.
/// Then resets the `Global` and runs a full GC again. See `hold_across_gc` in `ffi.h`.
Expand Down
Loading
Loading