Suggestion: simpler shared functions
dead-claudia opened this issue · 0 comments
Shared functions (and variables) could be the solution to the problem of methods, and if done correctly, could avoid most of the complexity of binding stuff to realms. They'd be heavily restricted, of course.
- Shared functions would be declared via the
shared
keyword, much likeasync
functions. - Shared variables would be declared like
shared var
,shared let
,shared const
, andshared using
. - Shared functions inherit from
SharedFunction.prototype
, isolating its linkage fully from unshared data. - Shared functions, outside their body, can only access shared variables and properties of the global object.
- The global object reference is that of the calling context's realm. This not only simplifies the calling sequence, but it also makes shared functions independently useful as a security primitive with realms.
- Shared functions can be
async
, in which they use the calling context's realm to get the rightPromise
reference to construct and return. - Shared variables have the same read and write rules as shared struct members.
- Shared structs' prototypes are themselves either null or shared structs. And yes, shared structs can
extend
other shared structs.- As per usual,
SharedStruct.prototype.constructor === SharedStruct
.
- As per usual,
- Shared struct constructors, static methods, and instance methods are implicitly
shared
. If you want to do things that can't beshared
, it can't be in ashared struct
definition. - Shared constants and read-only shared structs' properties do not require
unsafe
. Shared structs and their instance prototypes are themselves read-only, to improve ergonomics. - Built-in ES methods are only
shared
if explicitly stated as such. - To ease certain messaging use cases,
registered struct Type
and aregistered "name" struct Type
declaration exists to register a struct so that shared structs from other contexts can be matched to their local counterparts. A name can only be registered once, and a shared struct can only be registered once.- The name is configurable so minifiers and bundlers can still rename them and not either blow up bundle size or awkwardly dance around stuff.
Atomics.registeredKeyFor(Type)
andAtomics.registeredTypeFor("Type")
can inspect this registry.
Things shared variables and shared struct properties can be set to:
- Shared functions
- Shared arrays
- Shared structs
- Non-object values
When the shadow realms proposal comes around, those can also be added to this list, provided its methods employ the requisite locking to ensure it only runs on one calling agent at a time.
This should hopefully bring the flexibility needed. And with this API, I also propose the following modified API:
declare namespace Atomics {
// same as before, including other new extensions...
// New structs, all registered with internal names in reality
export registered internal struct UnlockToken {
constructor()
get locked(): boolean
unlock(): undefined
[Symbol.dispose](): undefined
}
export registered internal struct Mutex {
constructor()
lock(token?: UnlockToken): UnlockToken
lockIfAvailable(timeout: number, token?: UnlockToken): UnlockToken
}
export registered internal struct Condition {
constructor()
wait(token: UnlockToken): undefined
waitFor(token: UnlockToken, timeout: number, predicate?: () => boolean): undefined
notify(count?: number): undefined
}
}
An example counter object might look like this:
shared struct Counter {
value = 0
inc() {
let value = Atomics.load(this, "value")
while (true) {
let prev = Atomics.compareExchange(this, "value", value, value + 1)
if (prev === value) return value + 1
value = prev
}
}
}
StructRegistry.add(internalToken, Atomics.Mutex)
The code from https://github.com/tc39/proposal-structs/blob/main/ATTACHING-BEHAVIOR.md#coordinating-identity-continuity-among-workers could look like this instead:
// Inside worker A:
shared struct SharedThing {
x;
y;
foo() {
// Dynamically scoped to the caller's global, thus it always "just works"
console.log("do a foo");
}
};
let thing1 = new SharedThing;
workerB.postMessage(thing1);
// Inside worker B:
onmessage = (thing) => {
// undefined is not a function, and `SharedThing` didn't even need to be defined!
thing.foo();
};