use std::fmt::Debug;
use std::sync::Arc;
use dusk_core::abi::ContractId;
use dusk_wasmtime::{Engine, Instance, Memory, Module, Store, TypedFunc};
use parking_lot::RwLock;
use crate::{Error, JsonValue};
const OUTPUT_BUFFER_SIZE: i32 = 64 * 1024;
const ERROR_BUFFER_SIZE: i32 = 1024;
type CodecInputTuple = (i32, i32, i32, i32, i32, i32);
type CodecFfiFunc = TypedFunc<CodecInputTuple, i32>;
type GetterFfiFunc = TypedFunc<(i32, i32), i32>;
struct ReaderInner {
store: Store<()>,
memory: Memory,
alloc_fn: TypedFunc<i32, i32>,
dealloc_fn: TypedFunc<(i32, i32), ()>,
encode_input_fn_export: CodecFfiFunc,
decode_input_fn_export: CodecFfiFunc,
decode_output_fn_export: CodecFfiFunc,
decode_event_fn_export: CodecFfiFunc,
get_schema_fn: GetterFfiFunc,
get_version_fn: GetterFfiFunc,
get_last_error_fn: GetterFfiFunc,
}
#[derive(Clone)]
pub struct DriverReader {
inner: Arc<RwLock<ReaderInner>>,
contract_id: ContractId,
_instance: Arc<Instance>,
}
#[allow(clippy::missing_fields_in_debug)]
impl Debug for DriverReader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DriverReader")
.field("contract_id", &self.contract_id)
.finish()
}
}
impl DriverReader {
pub fn new(wasm_bytes: &[u8]) -> Result<Self, Error> {
Self::with_contract_id(wasm_bytes, ContractId::from_bytes([0u8; 32]))
}
#[allow(clippy::too_many_lines)]
pub fn with_contract_id(
wasm_bytes: &[u8],
contract_id: ContractId,
) -> Result<Self, Error> {
let engine = Engine::default();
let module = Module::from_binary(&engine, wasm_bytes).map_err(|e| {
Error::WasmRuntime(format!("Failed to compile WASM: {e}"))
})?;
let mut store = Store::new(&engine, ());
let instance =
Instance::new(&mut store, &module, &[]).map_err(|e| {
Error::WasmRuntime(format!("Failed to instantiate WASM: {e}"))
})?;
let memory =
instance.get_memory(&mut store, "memory").ok_or_else(|| {
Error::WasmExport("Missing 'memory' export".into())
})?;
let alloc_fn = instance
.get_typed_func::<i32, i32>(&mut store, "alloc")
.map_err(|e| {
Error::WasmExport(format!("Invalid 'alloc' export: {e}"))
})?;
let dealloc_fn = instance
.get_typed_func::<(i32, i32), ()>(&mut store, "dealloc")
.map_err(|e| {
Error::WasmExport(format!("Invalid 'dealloc' export: {e}"))
})?;
let encode_input_fn_export = instance
.get_typed_func::<CodecInputTuple, i32>(
&mut store,
"encode_input_fn",
)
.map_err(|e| {
Error::WasmExport(format!(
"Invalid 'encode_input_fn' export: {e}"
))
})?;
let decode_input_fn_export = instance
.get_typed_func::<CodecInputTuple, i32>(
&mut store,
"decode_input_fn",
)
.map_err(|e| {
Error::WasmExport(format!(
"Invalid 'decode_input_fn' export: {e}"
))
})?;
let decode_output_fn_export = instance
.get_typed_func::<CodecInputTuple, i32>(
&mut store,
"decode_output_fn",
)
.map_err(|e| {
Error::WasmExport(format!(
"Invalid 'decode_output_fn' export: {e}"
))
})?;
let decode_event_fn_export = instance
.get_typed_func::<CodecInputTuple, i32>(&mut store, "decode_event")
.map_err(|e| {
Error::WasmExport(format!("Invalid 'decode_event' export: {e}"))
})?;
let get_schema_fn = instance
.get_typed_func::<(i32, i32), i32>(&mut store, "get_schema")
.map_err(|e| {
Error::WasmExport(format!("Invalid 'get_schema' export: {e}"))
})?;
let get_version_fn = instance
.get_typed_func::<(i32, i32), i32>(&mut store, "get_version")
.map_err(|e| {
Error::WasmExport(format!("Invalid 'get_version' export: {e}"))
})?;
let get_last_error_fn = instance
.get_typed_func::<(i32, i32), i32>(&mut store, "get_last_error")
.map_err(|e| {
Error::WasmExport(format!(
"Invalid 'get_last_error' export: {e}"
))
})?;
let init_fn = instance
.get_typed_func::<(), ()>(&mut store, "init")
.map_err(|e| {
Error::WasmExport(format!("Invalid 'init' export: {e}"))
})?;
init_fn
.call(&mut store, ())
.map_err(|e| Error::FfiError(format!("init() failed: {e}")))?;
let inner = ReaderInner {
store,
memory,
alloc_fn,
dealloc_fn,
encode_input_fn_export,
decode_input_fn_export,
decode_output_fn_export,
decode_event_fn_export,
get_schema_fn,
get_version_fn,
get_last_error_fn,
};
Ok(Self {
inner: Arc::new(RwLock::new(inner)),
contract_id,
_instance: Arc::new(instance),
})
}
#[must_use]
pub fn contract_id(&self) -> ContractId {
self.contract_id
}
pub fn encode_input_fn(
&self,
fn_name: &str,
json: &str,
) -> Result<Vec<u8>, Error> {
self.call_with_name_and_data(
fn_name.as_bytes(),
json.as_bytes(),
|inner, args| {
inner.encode_input_fn_export.call(&mut inner.store, args)
},
"encode_input_fn",
)
}
pub fn decode_input_fn(
&self,
fn_name: &str,
rkyv_bytes: &[u8],
) -> Result<JsonValue, Error> {
self.decode_common(
fn_name.as_bytes(),
rkyv_bytes,
|inner, args| {
inner.decode_input_fn_export.call(&mut inner.store, args)
},
"decode_input_fn",
)
}
pub fn decode_output_fn(
&self,
fn_name: &str,
rkyv_bytes: &[u8],
) -> Result<JsonValue, Error> {
self.decode_common(
fn_name.as_bytes(),
rkyv_bytes,
|inner, args| {
inner.decode_output_fn_export.call(&mut inner.store, args)
},
"decode_output_fn",
)
}
pub fn decode_event(
&self,
event_name: &str,
rkyv_bytes: &[u8],
) -> Result<JsonValue, Error> {
self.decode_common(
event_name.as_bytes(),
rkyv_bytes,
|inner, args| {
inner.decode_event_fn_export.call(&mut inner.store, args)
},
"decode_event",
)
}
pub fn get_schema(&self) -> Result<JsonValue, Error> {
let bytes = self.call_simple_output(
|inner, args| inner.get_schema_fn.call(&mut inner.store, args),
"get_schema",
)?;
Self::bytes_to_json(bytes)
}
pub fn get_version(&self) -> Result<String, Error> {
let bytes = self.call_simple_output(
|inner, args| inner.get_version_fn.call(&mut inner.store, args),
"get_version",
)?;
String::from_utf8(bytes)
.map_err(|e| Error::Other(format!("Invalid UTF-8: {e}")))
}
fn decode_common<F>(
&self,
name_bytes: &[u8],
rkyv_bytes: &[u8],
call: F,
op_name: &str,
) -> Result<JsonValue, Error>
where
F: FnOnce(
&mut ReaderInner,
CodecInputTuple,
) -> Result<i32, dusk_wasmtime::Error>,
{
let bytes = self
.call_with_name_and_data(name_bytes, rkyv_bytes, call, op_name)?;
Self::bytes_to_json(bytes)
}
fn call_with_name_and_data<F>(
&self,
name_bytes: &[u8],
data_bytes: &[u8],
call: F,
op_name: &str,
) -> Result<Vec<u8>, Error>
where
F: FnOnce(
&mut ReaderInner,
CodecInputTuple,
) -> Result<i32, dusk_wasmtime::Error>,
{
let mut inner = self.inner.write();
let (name_ptr, name_len) =
Self::alloc_and_write(&mut inner, name_bytes)?;
let (data_ptr, data_len) =
Self::alloc_and_write(&mut inner, data_bytes)?;
let result = Self::run_with_output_buffer(
&mut inner,
|inner, out_ptr, out_size| {
call(
inner,
(name_ptr, name_len, data_ptr, data_len, out_ptr, out_size),
)
.map_err(|e| {
Error::FfiError(format!("{op_name} call failed: {e}"))
})
},
);
let _ = Self::dealloc(&mut inner, name_ptr, name_len);
let _ = Self::dealloc(&mut inner, data_ptr, data_len);
result
}
fn call_simple_output<F>(
&self,
call: F,
op_name: &str,
) -> Result<Vec<u8>, Error>
where
F: FnOnce(
&mut ReaderInner,
(i32, i32),
) -> Result<i32, dusk_wasmtime::Error>,
{
let mut inner = self.inner.write();
Self::run_with_output_buffer(&mut inner, |inner, out_ptr, out_size| {
call(inner, (out_ptr, out_size)).map_err(|e| {
Error::FfiError(format!("{op_name} call failed: {e}"))
})
})
}
fn alloc_and_write(
inner: &mut ReaderInner,
bytes: &[u8],
) -> Result<(i32, i32), Error> {
let len = i32::try_from(bytes.len())
.map_err(|_| Error::WasmMemory("Buffer too large".into()))?;
let ptr = inner
.alloc_fn
.call(&mut inner.store, len)
.map_err(|e| Error::WasmMemory(format!("alloc failed: {e}")))?;
if ptr <= 0 {
return Err(Error::WasmMemory(
"alloc returned null or negative".into(),
));
}
#[allow(clippy::cast_sign_loss)]
let start = ptr as usize;
#[allow(clippy::cast_sign_loss)]
let end = start + len as usize;
inner
.memory
.data_mut(&mut inner.store)
.get_mut(start..end)
.ok_or_else(|| {
Error::WasmMemory("Memory access out of bounds".into())
})?
.copy_from_slice(bytes);
Ok((ptr, len))
}
fn dealloc(
inner: &mut ReaderInner,
ptr: i32,
len: i32,
) -> Result<(), Error> {
inner
.dealloc_fn
.call(&mut inner.store, (ptr, len))
.map_err(|e| Error::WasmMemory(format!("dealloc failed: {e}")))
}
fn read_buffer(
inner: &ReaderInner,
ptr: i32,
buf_size: i32,
) -> Result<Vec<u8>, Error> {
if ptr < 0 || buf_size < 4 {
return Err(Error::WasmMemory("Invalid buffer parameters".into()));
}
#[allow(clippy::cast_sign_loss)]
let ptr_usize = ptr as usize;
#[allow(clippy::cast_sign_loss)]
let buf_size_usize = buf_size as usize;
let data = inner.memory.data(&inner.store);
let len_bytes =
data.get(ptr_usize..ptr_usize + 4).ok_or_else(|| {
Error::WasmMemory("Cannot read length prefix".into())
})?;
let actual_size = u32::from_le_bytes([
len_bytes[0],
len_bytes[1],
len_bytes[2],
len_bytes[3],
]) as usize;
if actual_size.saturating_add(4) > buf_size_usize {
return Err(Error::WasmMemory(format!(
"Buffer overflow: actual_size={actual_size}, buf_size={buf_size}"
)));
}
let payload_start = ptr_usize + 4;
let payload_end = payload_start + actual_size;
let payload = data
.get(payload_start..payload_end)
.ok_or_else(|| Error::WasmMemory("Cannot read payload".into()))?;
Ok(payload.to_vec())
}
fn get_last_error(inner: &mut ReaderInner) -> String {
let out_ptr =
match inner.alloc_fn.call(&mut inner.store, ERROR_BUFFER_SIZE) {
Ok(ptr) if ptr != 0 => ptr,
_ => return String::new(),
};
let _ = inner
.get_last_error_fn
.call(&mut inner.store, (out_ptr, ERROR_BUFFER_SIZE));
let result = Self::read_buffer(inner, out_ptr, ERROR_BUFFER_SIZE)
.and_then(|bytes| {
String::from_utf8(bytes)
.map_err(|e| Error::Other(format!("UTF-8 error: {e}")))
})
.unwrap_or_default();
let _ = Self::dealloc(inner, out_ptr, ERROR_BUFFER_SIZE);
result
}
fn run_with_output_buffer<F>(
inner: &mut ReaderInner,
f: F,
) -> Result<Vec<u8>, Error>
where
F: FnOnce(&mut ReaderInner, i32, i32) -> Result<i32, Error>,
{
let out_ptr = inner
.alloc_fn
.call(&mut inner.store, OUTPUT_BUFFER_SIZE)
.map_err(|e| Error::WasmMemory(format!("alloc failed: {e}")))?;
if out_ptr == 0 {
return Err(Error::WasmMemory("alloc returned null".into()));
}
let result = (|| {
let code = f(inner, out_ptr, OUTPUT_BUFFER_SIZE)?;
if code != 0 {
let err_msg = Self::get_last_error(inner);
return Err(Error::FfiError(format!(
"FFI returned {code}: {err_msg}"
)));
}
Self::read_buffer(inner, out_ptr, OUTPUT_BUFFER_SIZE)
})();
let _ = Self::dealloc(inner, out_ptr, OUTPUT_BUFFER_SIZE);
result
}
fn bytes_to_json(bytes: Vec<u8>) -> Result<JsonValue, Error> {
let s = String::from_utf8(bytes)
.map_err(|e| Error::Other(format!("Invalid UTF-8: {e}")))?;
serde_json::from_str(&s).map_err(Error::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constants() {
assert_eq!(OUTPUT_BUFFER_SIZE, 64 * 1024);
assert_eq!(ERROR_BUFFER_SIZE, 1024);
}
}