Closed zicklag closed 2 years ago
Sounds good, it seems useful to make this extensible. The biggest question is probably what kinds of parameters and return types to allow. T: Serialize + Deserialize
? This seems like it should work with v8
and wasm_bindgen
, but we probably want to be able to transmit ValueRef
s somehow. Maybe with #16 solved these will just be an index, so that should be good.
My current strategy has been to take make only one native Deno/Wasm Bindgen op, that has a signature in pseudo code like:
#[op]
fn op_bevy_mod_js_scripting(
op_name: String,
args: serde_json::Value
) -> Result<serde_json::Value, AnyError> {
// Here we check on the op-name and run the core/user op registered with that name.
}
That makes it super easy to register custom ops with our ops abstraction without having to generate new #[op]
annotated functions for Deno or else work with the function pointers manually.
And, yes, for ValueRef
s I'm going to try doing what I did for the WASM backend already and just pass handles into a SlotMap
so that gets us away from pointers and makes it easy to pass through a serde_json::Value
.
Haven't gotten far yet, but the concept seems to be sound so far.
Also, as a simple optimization I'm going to try to reduce the op_name
to an op_index
that will be mapped the string op_name
to the op_index
on the JavaScript side, so that we aren't having to pass strings over the FFI every time an op is called.
Just got the log operation implemented through the new abstraction. I think the API's turning out pretty nice, and it should allow us to implement runtime agnostic ops:
struct OpLog;
#[derive(Deserialize)]
struct OpLogArgs {
level: u8,
args: Vec<serde_json::Value>,
}
impl JsRuntimeOp for OpLog {
fn run(
&self,
script_info: &ScriptInfo,
_world: &mut World,
args: serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let args: OpLogArgs = serde_json::from_value(args).context("Parse `log` op args")?;
let level = args.level;
let text = args
.args
.iter()
.map(|arg| {
// Print string args without quotes
if let serde_json::Value::String(s) = &arg {
s.clone()
// Format other values as JSON
} else {
format!("{arg}")
}
})
.collect::<Vec<_>>()
.join(" ");
if level == 0 {
let _span = span!(Level::TRACE, "script", path = ?script_info.path).entered();
event!(target: "js_runtime", Level::TRACE, "{text}");
} else if level == 1 {
let _span = span!(Level::DEBUG, "script", path = ?script_info.path).entered();
event!(target: "js_runtime", Level::DEBUG, "{text}");
} else if level == 2 {
let _span = span!(Level::INFO, "script", path = ?script_info.path).entered();
event!(target: "js_runtime", Level::INFO, "{text}");
} else if level == 3 {
let _span = span!(Level::WARN, "script", path = ?script_info.path).entered();
event!(target: "js_runtime", Level::WARN, "{text}");
} else if level == 4 {
let _span = span!(Level::ERROR, "script", path = ?script_info.path).entered();
event!(target: "js_runtime", Level::ERROR, "{text}");
} else {
anyhow::bail!("Invalid log level");
}
Ok(serde_json::Value::Null)
}
fn js(&self) -> Option<&'static str> {
Some(
r#"
globalThis.trace = (...args) => bevyModJsScriptingOpSync("log", 0, args);
globalThis.debug = (...args) => bevyModJsScriptingOpSync("log", 1, args);
globalThis.info = (...args) => bevyModJsScriptingOpSync("log", 2, args);
globalThis.warn = (...args) => bevyModJsScriptingOpSync("log", 3, args);
globalThis.error = (...args) => bevyModJsScriptingOpSync("log", 4, args);
"#,
)
}
}
Got native runtime fully migrated to the new op abstraction and away from raw pointers in draft PR ( #19 ). Now we can actually #![forbid(unsafe_code)]
which pretty surprising for a plugin to implement scripting support!
I've still got to migrate WASM to the new system, which shouldn't be difficult because all the ops are now runtime agnostic and implemented in one place only.
First, though, I'm going to see about solving #16 real quick to make sure that will work well with the op abstraction and that it can stay runtime agnostic.
I'm realizing as I try to integrate this with my game that it could be important to make it as easy as possible for users to create their own custom ops.
Right now we have some holes in the functionality that should be built-in eventually, such as being able to interact with reflected
Vec
s andHashMap
s or being able toworld.spawn()
, but I also was thinking about the fact that I could definitely have some kind of functionality in my game that I wanted to expose more directly to my scripts, especially with reflected functions being work-in-progress.To this end, I think I'm going to experiment a little with a way to create an abstraction for creating the ops, and at the same time try to reduce the code duplication between the native and WASM runtimes by using that ops abstraction for all the core ops we currently have.