use std::collections::HashMap;
use std::sync::RwLock;
use rquickjs::Context;
use rquickjs::Function;
use rquickjs::Persistent;
use rquickjs::Runtime;
use polyplug::host_bridge::BridgeError;
use polyplug::host_bridge::RuntimeLanguageBridge;
use polyplug_abi::AbiError;
use polyplug_abi::AbiErrorCode;
use polyplug_abi::StringView;
use polyplug_abi::SupportedLanguage;
#[derive(Debug, thiserror::Error)]
pub enum JsBridgeError {
#[error("QuickJS runtime creation failed: {0}")]
RuntimeCreationFailed(String),
#[error("QuickJS context creation failed: {0}")]
ContextCreationFailed(String),
}
pub struct JsHostBridge {
runtime: Runtime,
context: Context,
contracts: RwLock<HashMap<u64, Persistent<Function<'static>>>>,
}
impl JsHostBridge {
pub fn new() -> Result<JsHostBridge, JsBridgeError> {
let runtime: Runtime = Runtime::new()
.map_err(|e: rquickjs::Error| JsBridgeError::RuntimeCreationFailed(e.to_string()))?;
let context: Context = Context::full(&runtime)
.map_err(|e: rquickjs::Error| JsBridgeError::ContextCreationFailed(e.to_string()))?;
Ok(JsHostBridge {
runtime,
context,
contracts: RwLock::new(HashMap::new()),
})
}
pub fn with_capacity(capacity: usize) -> Result<JsHostBridge, JsBridgeError> {
let runtime: Runtime = Runtime::new()
.map_err(|e: rquickjs::Error| JsBridgeError::RuntimeCreationFailed(e.to_string()))?;
let context: Context = Context::full(&runtime)
.map_err(|e: rquickjs::Error| JsBridgeError::ContextCreationFailed(e.to_string()))?;
Ok(JsHostBridge {
runtime,
context,
contracts: RwLock::new(HashMap::with_capacity(capacity)),
})
}
pub fn context(&self) -> &Context {
&self.context
}
pub fn runtime(&self) -> &Runtime {
&self.runtime
}
}
impl Drop for JsHostBridge {
fn drop(&mut self) {
if let Ok(mut contracts) = self.contracts.write() {
contracts.clear();
}
}
}
impl RuntimeLanguageBridge for JsHostBridge {
fn runtime_type(&self) -> SupportedLanguage {
SupportedLanguage::JavaScript
}
fn register_host_contract(
&mut self,
contract_id: u64,
implementation: Box<dyn core::any::Any>,
) -> Result<(), BridgeError> {
let callable: Persistent<Function<'static>> = implementation
.downcast::<Persistent<Function<'static>>>()
.map_err(|_| BridgeError::TypeMismatch {
contract_id,
expected: "Persistent<Function<'static>>".to_owned(),
got: "unknown type".to_owned(),
})
.map(|boxed| *boxed)?;
let mut contracts: std::sync::RwLockWriteGuard<
'_,
HashMap<u64, Persistent<Function<'static>>>,
> = self
.contracts
.write()
.map_err(|_| BridgeError::VmRegistrationFailed {
contract_id,
reason: "failed to acquire write lock on contracts map".to_owned(),
})?;
if contracts.contains_key(&contract_id) {
return Err(BridgeError::DuplicateContract { contract_id });
}
contracts.insert(contract_id, callable);
Ok(())
}
unsafe fn call_host_contract(
&self,
contract_id: u64,
fn_id: u32,
args: *const (),
out: *mut (),
) -> AbiError {
let contracts_guard: std::sync::RwLockReadGuard<
'_,
HashMap<u64, Persistent<Function<'static>>>,
> = match self.contracts.read() {
Ok(guard) => guard,
Err(_) => {
return AbiError {
code: AbiErrorCode::HostContractCallFailed as u32,
message: StringView::from_static(
b"failed to acquire read lock on contracts map",
),
};
}
};
let callable: &Persistent<Function<'static>> = match contracts_guard.get(&contract_id) {
Some(f) => f,
None => {
return AbiError {
code: AbiErrorCode::HostContractNotFound as u32,
message: StringView::from_static(b"host contract not found"),
};
}
};
let fn_id_arg: u32 = fn_id;
let args_ptr: i64 = args as usize as i64;
let out_ptr: i64 = out as usize as i64;
let call_result: Result<i32, rquickjs::Error> = self.context.with(|ctx| {
let js_fn: Function<'_> = callable.clone().restore(&ctx)?;
let args_bigint: rquickjs::BigInt<'_> =
rquickjs::BigInt::from_i64(ctx.clone(), args_ptr)?;
let out_bigint: rquickjs::BigInt<'_> =
rquickjs::BigInt::from_i64(ctx.clone(), out_ptr)?;
let result: i32 = js_fn
.call::<(u32, rquickjs::BigInt<'_>, rquickjs::BigInt<'_>), i32>((
fn_id_arg,
args_bigint,
out_bigint,
))?;
Ok(result)
});
match call_result {
Ok(0) => AbiError::ok(),
Ok(code) => AbiError {
code: code as u32,
message: StringView::null(),
},
Err(e) => {
let message: String = format!("JavaScript exception: {}", e);
let message_static: &'static str = Box::leak(message.into_boxed_str());
AbiError {
code: AbiErrorCode::HostContractCallFailed as u32,
message: StringView {
ptr: message_static.as_ptr(),
len: message_static.len(),
},
}
}
}
}
}
unsafe impl Send for JsHostBridge {}
unsafe impl Sync for JsHostBridge {}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
#[test]
fn bridge_new_creates_empty_bridge() {
let bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let contracts: std::sync::RwLockReadGuard<'_, HashMap<u64, Persistent<Function<'static>>>> =
bridge.contracts.read().expect("read lock");
assert!(contracts.is_empty());
}
#[test]
fn bridge_with_capacity_creates_empty_bridge() {
let bridge: JsHostBridge = JsHostBridge::with_capacity(10).expect("bridge creation");
let contracts: std::sync::RwLockReadGuard<'_, HashMap<u64, Persistent<Function<'static>>>> =
bridge.contracts.read().expect("read lock");
assert!(contracts.is_empty());
}
#[test]
fn bridge_runtime_type_returns_javascript() {
let bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
assert_eq!(bridge.runtime_type(), SupportedLanguage::JavaScript);
}
#[test]
fn bridge_context_returns_reference() {
let bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let ctx: &Context = bridge.context();
let result: i32 = ctx.with(|ctx| ctx.eval::<i32, _>("42")).expect("eval");
assert_eq!(result, 42);
}
#[test]
fn bridge_runtime_returns_reference() {
let bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let _runtime: &Runtime = bridge.runtime();
}
#[test]
fn bridge_register_host_contract_success() {
let mut bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let persistent: Persistent<Function<'static>> = bridge.context().with(|ctx| {
let callable: Function<'_> = ctx
.eval::<Function<'_>, _>("(function(fn_id, args, out) { return 0; })")
.expect("eval function");
Persistent::save(&ctx, callable)
});
let result: Result<(), BridgeError> =
bridge.register_host_contract(1234, Box::new(persistent));
assert!(result.is_ok());
let contracts: std::sync::RwLockReadGuard<'_, HashMap<u64, Persistent<Function<'static>>>> =
bridge.contracts.read().expect("read lock");
assert!(contracts.contains_key(&1234));
}
#[test]
fn bridge_register_host_contract_duplicate_fails() {
let mut bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let persistent1: Persistent<Function<'static>> = bridge.context().with(|ctx| {
let callable: Function<'_> = ctx
.eval::<Function<'_>, _>("(function(fn_id, args, out) { return 0; })")
.expect("eval function");
Persistent::save(&ctx, callable)
});
let persistent2: Persistent<Function<'static>> = bridge.context().with(|ctx| {
let callable: Function<'_> = ctx
.eval::<Function<'_>, _>("(function(fn_id, args, out) { return 1; })")
.expect("eval function");
Persistent::save(&ctx, callable)
});
let result1: Result<(), BridgeError> =
bridge.register_host_contract(1234, Box::new(persistent1));
assert!(result1.is_ok());
let result2: Result<(), BridgeError> =
bridge.register_host_contract(1234, Box::new(persistent2));
assert!(result2.is_err());
let err: BridgeError = result2.expect_err("should fail");
assert!(matches!(
err,
BridgeError::DuplicateContract { contract_id: 1234 }
));
}
#[test]
fn bridge_register_host_contract_type_mismatch_fails() {
let mut bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let result: Result<(), BridgeError> = bridge.register_host_contract(1234, Box::new(42i32));
assert!(result.is_err());
let err: BridgeError = result.expect_err("should fail");
assert!(matches!(
err,
BridgeError::TypeMismatch {
contract_id: 1234,
..
}
));
}
#[test]
fn bridge_call_host_contract_not_found() {
let bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let result: AbiError =
unsafe { bridge.call_host_contract(9999, 0, core::ptr::null(), core::ptr::null_mut()) };
assert_eq!(result.code, AbiErrorCode::HostContractNotFound as u32);
}
#[test]
fn bridge_call_host_contract_success() {
let mut bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let persistent: Persistent<Function<'static>> = bridge.context().with(|ctx| {
let callable: Function<'_> = ctx
.eval::<Function<'_>, _>("(function(fn_id, args, out) { return 0; })")
.expect("eval function");
Persistent::save(&ctx, callable)
});
bridge
.register_host_contract(1234, Box::new(persistent))
.expect("register");
let result: AbiError =
unsafe { bridge.call_host_contract(1234, 5, core::ptr::null(), core::ptr::null_mut()) };
assert!(result.is_ok());
}
#[test]
fn bridge_call_host_contract_returns_error_code() {
let mut bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let persistent: Persistent<Function<'static>> = bridge.context().with(|ctx| {
let callable: Function<'_> = ctx
.eval::<Function<'_>, _>("(function(fn_id, args, out) { return 42; })")
.expect("eval function");
Persistent::save(&ctx, callable)
});
bridge
.register_host_contract(1234, Box::new(persistent))
.expect("register");
let result: AbiError =
unsafe { bridge.call_host_contract(1234, 0, core::ptr::null(), core::ptr::null_mut()) };
assert_eq!(result.code, 42_u32);
}
#[test]
fn bridge_call_host_contract_exception() {
let mut bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let persistent: Persistent<Function<'static>> = bridge.context().with(|ctx| {
let callable: Function<'_> = ctx
.eval::<Function<'_>, _>(
"(function(fn_id, args, out) { throw new Error('test error'); })",
)
.expect("eval function");
Persistent::save(&ctx, callable)
});
bridge
.register_host_contract(1234, Box::new(persistent))
.expect("register");
let result: AbiError =
unsafe { bridge.call_host_contract(1234, 0, core::ptr::null(), core::ptr::null_mut()) };
assert_eq!(result.code, AbiErrorCode::HostContractCallFailed as u32);
}
#[test]
fn bridge_call_host_contract_with_fn_id() {
let mut bridge: JsHostBridge = JsHostBridge::new().expect("bridge creation");
let persistent: Persistent<Function<'static>> = bridge.context().with(|ctx| {
let callable: Function<'_> = ctx
.eval::<Function<'_>, _>("(function(fn_id, args, out) { return fn_id * 2; })")
.expect("eval function");
Persistent::save(&ctx, callable)
});
bridge
.register_host_contract(1234, Box::new(persistent))
.expect("register");
let result: AbiError =
unsafe { bridge.call_host_contract(1234, 5, core::ptr::null(), core::ptr::null_mut()) };
assert_eq!(result.code, 10_u32);
}
}