Skip to content
Merged
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
28 changes: 18 additions & 10 deletions contracts/embedder-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,15 +304,23 @@ leaks (docs/architecture.md §7).
**Host-implemented** (guest holds handles): the host supplies a class
implementing the bindgen interface (camelCase methods, statics as static
members, the WIT constructor as the JS constructor). The runtime owns the
instance↔rep mapping; when the guest drops its last own handle the runtime calls
`instance[Symbol.dispose]?.()`. Method `self` is the instance.

Overlapping host-originated borrows retain the mapping until the last borrowing
call ends. A guest drop during that interval defers disposal until the final
borrow ends; the pending-drop instance cannot be passed as own again. A deferred
disposal error is reported by the last borrowing call, after all its borrow
mappings are released. An existing call failure remains primary; results that
cannot be delivered because cleanup failed are released rather than abandoned.
rep→instance registrations; method `self` is the instance. Every time a plain
host object is passed as `own<R>`, the runtime creates a fresh resource
registration. Registrations are independent even when they use the same JS
object as backing data: dropping each one calls `instance[Symbol.dispose]?.()`.
Sharing or deduplicating the backing data is the host implementation's
responsibility.

Passing an existing guest-resource wrapper as `own<R>` is different: it
transfers that one resource and invalidates the wrapper; it does not create an
independent resource. Canonical handle lender rules continue to protect borrows
of such concrete resources from transfer or destruction while lent.

Each host-originated `borrow<R>` of a plain host object gets a fresh,
call-scoped registration, independent of other borrows and owns backed by that
object. Ending that scope removes only its temporary registration and never
disposes the object. Cleanup still releases every temporary mapping after the
call, including failure paths.

**Constructors are synchronous** (a JS constructor cannot await). A guest
constructor that does not complete synchronously raises a named error rather
Expand All @@ -325,7 +333,7 @@ deferred until demanded.
| host receives `own<R>` | new instance; host owns it (drop/`using`) | the host's own instance; the guest's handle is gone; no dispose call |
| host receives `borrow<R>` | valid only during the call (retention throws) | the host's own instance; scoping is guest-side bookkeeping |
| host passes `own<R>` | wrapper invalidated (transferred) | instance registered; guest owns the handle |
| host passes `borrow<R>` | wrapper stays valid | an unregistered instance gets a rep for the call's duration |
| host passes `borrow<R>` | wrapper stays valid | every borrow gets a fresh rep for that call's duration |

### Pattern (non-normative): binding platform classes directly

Expand Down
2 changes: 1 addition & 1 deletion protocol/deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@polyengine/protocol",
"version": "0.3.2",
"version": "0.4.0",
"exports": {
".": "./src/mod.ts"
},
Expand Down
108 changes: 18 additions & 90 deletions runtime/src/embedder/instantiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import type { LoadedPlan } from "../plan/loader.ts";
import { loadEnvelope, loadPlan, PlanError } from "../plan/loader.ts";
import type { FuncType, ResourceTypeInfo, ValType } from "../cabi/types.ts";
import type { ComponentValue, VariantValue } from "../cabi/types.ts";
import { despecialize } from "../cabi/types.ts";
import { hostFutureFor, hostStreamFor } from "../exec/host_streams.ts";
import { Trap } from "../cabi/trap.ts";
import {
type ComponentHandle,
Expand Down Expand Up @@ -428,8 +426,10 @@ class Facade {
lowerBorrow: (v, t) => {
const b = this.#binding(t.rt.resource);
if (b.kind === "host") {
// Each overlapping call retains the rep; the final borrow release
// removes only temporary mappings, never a guest-owned registration.
// A plain host object does not identify any existing resource.
// Each lowering therefore creates its own call-scoped borrow
// registration; canonical guest-handle lends are tracked below this
// facade (definitions.py:1794-1800).
const { rep, release } = b.registry.borrowFor(v);
if (this.#lowerScope === null) release();
else this.#lowerScope.push(release);
Expand Down Expand Up @@ -596,8 +596,8 @@ class Facade {
this.#pendingHostResources.push({ importIndex, registry, cls });
return hostResourceType({
name: leaf.leaf,
// The guest dropped its last own handle: run the destructor, which for
// a host-implemented resource is `instance[Symbol.dispose]?.()`.
// The guest dropped this owning resource registration: run its
// destructor, which is `instance[Symbol.dispose]?.()` for a host resource.
dtor: (rep) => registry.dtor(rep),
});
}
Expand Down Expand Up @@ -978,8 +978,8 @@ class Facade {
* Lower a call's arguments, collecting the releases for anything that was
* allocated *for the duration of this call* (see `lowerBorrow`).
*
* Save/restore the collection slot for reentrant lowering. Release every
* borrow once, even if another release throws, then report the first error.
* Save/restore the collection slot for reentrant lowering. Releases are
* non-throwing and each borrow is released once.
*/
#lowerParams(
params: ValType[],
Expand All @@ -991,93 +991,22 @@ class Facade {
const release = () => {
if (released) return;
released = true;
let failed = false;
let error: unknown;
for (const r of scope) {
try {
r();
} catch (e) {
if (!failed) error = e;
failed = true;
}
}
if (failed) throw error;
for (const r of scope) r();
};
const outer = this.#lowerScope;
this.#lowerScope = scope;
let lowered: ComponentValue[];
try {
lowered = params.map((p, i) => fromHost(args[i], p, o));
} catch (e) {
try {
release();
} catch {
// The original error wins; a secondary failure of the unwind is
// not the story.
}
release();
throw e;
} finally {
this.#lowerScope = outer;
}
return { lowered, release };
}

/** Cleanup cannot abandon a result already transferred out of the guest. */
#finishCall(
release: () => void,
succeeded: boolean,
raw: unknown,
type: ValType | null,
): void {
try {
release();
} catch (e) {
if (!succeeded) return; // Preserve the original call failure.
if (type !== null) this.#dropResult(raw as ComponentValue, type);
throw e;
}
}

#dropResult(raw: ComponentValue, type: ValType): void {
// Already failing cleanup: retire every owned leaf, preserving that error
// even when a result destructor also throws.
try {
const t = despecialize(type);
switch (t.kind) {
case "own":
this.#bridge.dropOwn(raw as number, t);
break;
case "future":
hostFutureFor(raw).drop();
break;
case "stream":
hostStreamFor(raw).readable.drop();
break;
case "list":
for (const v of raw as ComponentValue[]) {
this.#dropResult(v, t.element);
}
break;
case "record":
for (const f of t.fields) {
this.#dropResult(
(raw as Record<string, ComponentValue>)[f.label],
f.type,
);
}
break;
case "variant": {
const v = raw as VariantValue;
const payload = t.cases.find((c) => c.label === v.kind)?.type;
if (payload != null) this.#dropResult(v.value, payload);
break;
}
}
} catch {
// The argument cleanup error remains primary.
}
}

/**
* Wrap one lifted export.
*
Expand Down Expand Up @@ -1113,14 +1042,13 @@ class Facade {
try {
pending = Promise.resolve(fn(...lowered)) as Promise<ComponentValue>;
} catch (e) {
this.#finishCall(release, false, undefined, resultType);
release();
throw e;
}
return Future.deferred(
pending,
elementCodec(element, o),
(succeeded, raw) =>
this.#finishCall(release, succeeded, raw, resultType),
() => release(),
) as unknown as Promise<unknown>;
};
} else {
Expand All @@ -1135,10 +1063,10 @@ class Facade {
try {
raw = await fn(...lowered);
} catch (e) {
this.#finishCall(release, false, undefined, resultType);
release();
throw e;
}
this.#finishCall(release, true, raw, resultType);
release();
if (resultType === null) return undefined;
if (resultType.kind === "result") {
// Internal result: `{kind: "ok"|"error", value}` (cabi/types.ts
Expand Down Expand Up @@ -1217,10 +1145,10 @@ class Facade {
try {
raw = entry(...lowered);
} catch (e) {
this.#finishCall(release, false, undefined, resultType);
release();
throw e;
}
this.#finishCall(release, true, raw, resultType);
release();
if (isThenable(raw)) unreachableThenable(raw);
return Future.fromLifted(
raw as ComponentValue,
Expand All @@ -1239,10 +1167,10 @@ class Facade {
try {
raw = entry(...lowered);
} catch (e) {
this.#finishCall(release, false, undefined, resultType);
release();
throw e;
}
this.#finishCall(release, true, raw, resultType);
release();
if (isThenable(raw)) unreachableThenable(raw);
if (resultType === null) return undefined;
if (resultType.kind === "result") {
Expand Down
Loading
Loading