Created
August 5, 2019 23:45
-
-
Save piscisaureus/5f1fc257e57cb976ec446eed03fb76f5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::collections::HashMap; | |
use std::sync::Mutex; | |
#[macro_use] | |
extern crate lazy_static; | |
struct CoreOp {} // Placeholder. | |
struct FlatBuffer {} // Placeholder. | |
struct PinnedBuf {} // Placeholder. | |
impl FlatBuffer { | |
fn parse(_blob: &[u8]) -> Self { | |
unimplemented!() | |
} | |
} | |
// Use a separate trait to give the op a name, because a trait object | |
// object (dyn Opdispatcher) can't have associated constants. | |
trait NamedOpDispatcher: OpDispatcher { | |
const NAME: &'static str; | |
} | |
trait OpDispatcher: Send + Sync { | |
fn dispatch(&self, args: &[u8], buf: Option<PinnedBuf>) -> CoreOp; | |
} | |
trait FlatBufferOpDispatcher: Send + Sync { | |
fn dispatch_flatbuffer(&self, fb: &FlatBuffer, buf: Option<PinnedBuf>) -> CoreOp; | |
} | |
impl<T> OpDispatcher for T | |
where | |
T: FlatBufferOpDispatcher, | |
{ | |
fn dispatch(&self, args: &[u8], buf: Option<PinnedBuf>) -> CoreOp { | |
let fb = FlatBuffer::parse(args); // Or something like that. | |
return self.dispatch_flatbuffer(&fb, buf); | |
} | |
} | |
fn register_op<D: NamedOpDispatcher + 'static>(d: D) { | |
// Rather than having a global registry, I think it should be per-isolate. | |
lazy_static! { | |
static ref OP_REGISTRY: Mutex<HashMap<&'static str, Box<dyn OpDispatcher>>> = | |
Mutex::new(HashMap::new()); | |
} | |
OP_REGISTRY | |
.lock() | |
.unwrap() | |
.entry(D::NAME) | |
.and_modify(|_| panic!("Op already registered")) | |
.or_insert(Box::new(d)); | |
} | |
// Example implements OpDispatcher. | |
struct BasicOpDispatcherExample; | |
impl NamedOpDispatcher for BasicOpDispatcherExample { | |
const NAME: &'static str = "Basic"; | |
} | |
impl OpDispatcher for BasicOpDispatcherExample { | |
fn dispatch(&self, _args: &[u8], _buf: Option<PinnedBuf>) -> CoreOp { | |
unimplemented!() | |
} | |
} | |
// Example implements FlatBufferOpDispatcher. | |
struct SomeFlatBufferOpDispatcher; | |
impl NamedOpDispatcher for SomeFlatBufferOpDispatcher { | |
const NAME: &'static str = "FB"; | |
} | |
impl FlatBufferOpDispatcher for SomeFlatBufferOpDispatcher { | |
fn dispatch_flatbuffer(&self, _fb: &FlatBuffer, _buf: Option<PinnedBuf>) -> CoreOp { | |
unimplemented!() | |
} | |
} | |
fn main() { | |
register_op(BasicOpDispatcherExample); | |
register_op(SomeFlatBufferOpDispatcher); | |
register_op(SomeFlatBufferOpDispatcher); // Crash. | |
} |
my idea was that you'd register concrete instances of dispatchers
I think that's reasonable.
Object.freeze(Deno.core.ops);
I think that would introduce problems for dlopen.
Object.freeze(Deno.core.ops);
I think that would introduce problems for dlopen.
You're right. Then probably some getter/setter structure would make sense. The idea is to prevent users from meddling with Deno.core.ops
, but yeah, ability to register additional ops dynamically is required.
Namespacing maybe?
type OpsObject = { [namespace: string]: { [name: string]: number } };
class Ops {
private readonly records: OpsObject = {};
// Cloned version of records that is frozen, so we can avoid cloning records for every access.
private recordsFrozen: OpsObject = Object.freeze(Object.assign({}, this.records));
constructor() {}
get ids(): OpsObject {
return this.recordsFrozen;
}
add(namespace: string, name: string, id: number) {
if (this.records[namespace] === undefined) {
this.records[namespace] = {};
}
this.records[namespace][name] = id;
// Clone, freeze, and assign records to recordsFrozen.
this.recordsFrozen = Object.freeze(Object.assign({}, this.records));
}
}
Deno.core.ops = new Ops();
Object.seal(Deno.core.ops);
// during startup
Deno.core.registerOp = Deno.core.ops.add;
// files.ts
function read(fd: number, buf: Uint8Array): number {
/* ... */
Deno.core.dispatch(Deno.core.ops.ids.builtin.read, controlbuf, zerobuf);
/* ... */
}
New dlopen/plugins code would be something like this:
export type PluginCallReturn = Uint8Array | undefined;
export interface PluginOp {
dispatch(
data: Uint8Array,
zeroCopy?: ArrayBufferView
): Promise<PluginCallReturn> | PluginCallReturn;
}
class PluginOpImpl implements PluginOp {
constructor(
private readonly pluginRid: number,
private readonly name: string,
) {
// assert op id is present
assert(Deno.core.ops.ids["plugin_" + this.pluginRid][name] !== undefined);
}
dispatch(
data: Uint8Array,
zeroCopy?: ArrayBufferView
): Promise<PluginCallReturn> | PluginCallReturn {
const response = Deno.core.dispatch(Deno.core.ops.ids["plugin_" + this.pluginRid][name], data, zeroCopy);
// handle response(create promise if async)
}
}
export interface Plugin {
loadOp(name: string): PluginOp;
}
export class PluginImpl implements Plugin {
// unique resource identifier for the loaded dynamic lib rust side
private readonly rid: number;
private readonly opSet: Set<string> = new Set();
constructor(libraryPath: string) {
this.rid = dlOpen(libraryPath);
}
loadOp(name: string): PluginOp {
if (!this.opSet.has(name)) {
// Ensure op is registed and Deno.core.registerOp gets called with the op id.
loadPluginOp(this.rid, name);
this.opSet.add(name);
}
let op = new PluginOpImpl(this.rid, name);
return op;
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Agreed, that would solve issue with access to special ops for TS compiler isolate (
op_fetch_module_meta_data
,op_cache
).I'm not sure about
register_op
- you suggested to passNamedOpDispatcher
as an argument and registers a single op. This seems cumbersome, my idea was that you'd register concrete instances of dispatchers that in turn have mapping ofop -> handler
. During registration of dispatcher it's verified that this dispatcher wasn't registered before, as well as verified there are no duplicate op names.So there's two layers of registries, but that shouldn't really hurt performance because registration will happen only once on startup. Advantage: ops can be nicely grouped by domain in a single dispatcher.
Then during Deno startup we can send full mapping with ops
So instead of this:
We'd get this:
WDYT? CC @piscisaureus @ry