#![deny(missing_docs)]
#![warn(clippy::missing_errors_doc)]
#![warn(clippy::missing_panics_doc)]
#![warn(clippy::doc_markdown)]
#![allow(non_camel_case_types)]
#![allow(unsafe_code)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]
pub mod arena;
pub mod convert;
mod macros;
pub use convert::{
lift_byte_slice, lift_bytes, lift_opt_scalar, lift_opt_string, lift_ptr_vec, lift_scalar_vec,
lift_string_vec, lower_bytes, lower_opt_scalar, lower_opt_string, lower_ptr_vec,
lower_scalar_vec, lower_string_vec, write_map_out,
};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
pub type weaveffi_handle_t = u64;
#[repr(C)]
#[derive(Debug)]
pub struct weaveffi_error {
pub code: i32,
pub message: *const c_char,
}
impl Default for weaveffi_error {
fn default() -> Self {
Self {
code: 0,
message: ptr::null(),
}
}
}
pub fn error_set_ok(out_err: *mut weaveffi_error) {
if out_err.is_null() {
return;
}
let err = unsafe { &mut *out_err };
if !err.message.is_null() {
unsafe { drop(CString::from_raw(err.message as *mut c_char)) };
}
err.code = 0;
err.message = ptr::null();
}
#[allow(clippy::missing_panics_doc)]
pub fn error_set(out_err: *mut weaveffi_error, code: i32, message: &str) {
if out_err.is_null() {
return;
}
let err = unsafe { &mut *out_err };
if !err.message.is_null() {
unsafe { drop(CString::from_raw(err.message as *mut c_char)) };
}
err.code = code;
let owned_message = message.replace('\0', "");
let cstr = CString::new(owned_message).expect("CString::new sanitized input");
err.message = cstr.into_raw();
}
pub trait ErrorReport {
fn code(&self) -> i32 {
-1
}
fn message(&self) -> String;
}
impl<E: std::fmt::Display> ErrorReport for E {
fn message(&self) -> String {
self.to_string()
}
}
pub fn result_to_out_err<T, E: ErrorReport>(
result: Result<T, E>,
out_err: *mut weaveffi_error,
) -> Option<T> {
match result {
Ok(value) => {
error_set_ok(out_err);
Some(value)
}
Err(e) => {
error_set(out_err, e.code(), &e.message());
None
}
}
}
pub const PANIC_ERROR_CODE: i32 = -2;
pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"producer panicked".to_string()
}
}
pub fn error_set_panic(out_err: *mut weaveffi_error, payload: &(dyn std::any::Any + Send)) {
error_set(
out_err,
PANIC_ERROR_CODE,
&format!("producer panicked: {}", panic_message(payload)),
);
}
#[allow(clippy::missing_panics_doc)]
pub fn string_to_c_ptr(s: impl AsRef<str>) -> *const c_char {
let s = s.as_ref();
let sanitized = if s.as_bytes().contains(&0) {
s.replace('\0', "")
} else {
s.to_owned()
};
let cstr = CString::new(sanitized).expect("string_to_c_ptr: unexpected NUL after sanitization");
cstr.into_raw()
}
pub fn free_string(ptr: *const c_char) {
if ptr.is_null() {
return;
}
unsafe { drop(CString::from_raw(ptr as *mut c_char)) };
}
pub fn free_bytes(ptr: *mut u8, len: usize) {
if ptr.is_null() {
return;
}
unsafe { drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len))) };
}
#[cfg(target_arch = "wasm32")]
const WASM_ALLOC_ALIGN: usize = 8;
#[cfg(target_arch = "wasm32")]
pub fn wasm_alloc(size: usize) -> *mut u8 {
let size = size.max(1);
let layout = std::alloc::Layout::from_size_align(size, WASM_ALLOC_ALIGN)
.expect("weaveffi_alloc: invalid layout");
unsafe { std::alloc::alloc(layout) }
}
#[cfg(target_arch = "wasm32")]
pub fn wasm_dealloc(ptr: *mut u8, size: usize) {
if ptr.is_null() {
return;
}
let size = size.max(1);
let layout = std::alloc::Layout::from_size_align(size, WASM_ALLOC_ALIGN)
.expect("weaveffi_dealloc: invalid layout");
unsafe { std::alloc::dealloc(ptr, layout) };
}
pub fn error_clear(err: *mut weaveffi_error) {
error_set_ok(err);
}
#[repr(C)]
pub struct weaveffi_cancel_token {
cancelled: AtomicBool,
}
pub fn cancel_token_create() -> *mut weaveffi_cancel_token {
Box::into_raw(Box::new(weaveffi_cancel_token {
cancelled: AtomicBool::new(false),
}))
}
pub fn cancel_token_cancel(token: *mut weaveffi_cancel_token) {
if token.is_null() {
return;
}
let t = unsafe { &*token };
t.cancelled.store(true, Ordering::Release);
}
pub fn cancel_token_is_cancelled(token: *const weaveffi_cancel_token) -> bool {
if token.is_null() {
return false;
}
let t = unsafe { &*token };
t.cancelled.load(Ordering::Acquire)
}
pub fn cancel_token_destroy(token: *mut weaveffi_cancel_token) {
if token.is_null() {
return;
}
unsafe { drop(Box::from_raw(token)) };
}
pub struct CancelToken {
raw: *const weaveffi_cancel_token,
}
unsafe impl Send for CancelToken {}
unsafe impl Sync for CancelToken {}
impl CancelToken {
#[doc(hidden)]
#[must_use]
pub fn from_raw(raw: *const weaveffi_cancel_token) -> Self {
Self { raw }
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
cancel_token_is_cancelled(self.raw)
}
}
pub struct Iter<T> {
inner: Box<dyn Iterator<Item = T> + Send>,
}
impl<T> Iter<T> {
pub fn new<I>(iter: I) -> Self
where
I: IntoIterator<Item = T>,
I::IntoIter: Send + 'static,
{
Self {
inner: Box::new(iter.into_iter()),
}
}
}
impl<T> Iterator for Iter<T> {
type Item = T;
fn next(&mut self) -> Option<T> {
self.inner.next()
}
}
pub fn block_on<F: std::future::Future>(fut: F) -> F::Output {
use std::sync::Arc;
use std::task::{Context, Poll, Wake, Waker};
use std::thread::{self, Thread};
struct ThreadWaker(Thread);
impl Wake for ThreadWaker {
fn wake(self: Arc<Self>) {
self.0.unpark();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.unpark();
}
}
let mut fut = Box::pin(fut);
let waker = Waker::from(Arc::new(ThreadWaker(thread::current())));
let mut cx = Context::from_waker(&waker);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(out) => return out,
Poll::Pending => thread::park(),
}
}
}
pub fn c_ptr_to_string(ptr: *const c_char) -> Option<String> {
if ptr.is_null() {
return None;
}
let c = unsafe { CStr::from_ptr(ptr) };
c.to_str().ok().map(|s| s.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_roundtrip_and_free() {
let ptr = string_to_c_ptr("hello world");
assert!(!ptr.is_null());
let recovered = c_ptr_to_string(ptr).unwrap();
assert_eq!(recovered, "hello world");
free_string(ptr);
}
#[test]
fn free_string_null_is_safe() {
free_string(ptr::null());
}
#[test]
fn free_bytes_null_is_safe() {
free_bytes(ptr::null_mut(), 0);
}
#[test]
fn bytes_alloc_and_free() {
let data: Vec<u8> = vec![1, 2, 3, 4, 5];
let len = data.len();
let boxed = data.into_boxed_slice();
let ptr = Box::into_raw(boxed) as *mut u8;
free_bytes(ptr, len);
}
#[test]
fn error_default_is_ok() {
let err = weaveffi_error::default();
assert_eq!(err.code, 0);
assert!(err.message.is_null());
}
#[test]
fn error_set_and_clear() {
let mut err = weaveffi_error::default();
error_set(&mut err, -1, "something went wrong");
assert_eq!(err.code, -1);
assert!(!err.message.is_null());
let msg = c_ptr_to_string(err.message).unwrap();
assert_eq!(msg, "something went wrong");
error_clear(&mut err);
assert_eq!(err.code, 0);
assert!(err.message.is_null());
}
#[test]
fn error_clear_null_is_safe() {
error_clear(ptr::null_mut());
}
#[test]
fn error_set_ok_frees_prior_message() {
let mut err = weaveffi_error::default();
error_set(&mut err, 1, "first");
error_set_ok(&mut err);
assert_eq!(err.code, 0);
assert!(err.message.is_null());
}
#[test]
fn error_set_replaces_prior_message() {
let mut err = weaveffi_error::default();
error_set(&mut err, 1, "first");
error_set(&mut err, 2, "second");
assert_eq!(err.code, 2);
let msg = c_ptr_to_string(err.message).unwrap();
assert_eq!(msg, "second");
error_clear(&mut err);
}
#[test]
fn result_to_out_err_ok_path() {
let mut err = weaveffi_error::default();
let val: Result<i32, String> = Ok(42);
let opt = result_to_out_err(val, &mut err);
assert_eq!(opt, Some(42));
assert_eq!(err.code, 0);
assert!(err.message.is_null());
}
#[test]
fn result_to_out_err_error_path() {
let mut err = weaveffi_error::default();
let val: Result<i32, String> = Err("bad input".to_string());
let opt = result_to_out_err(val, &mut err);
assert_eq!(opt, None);
assert_eq!(err.code, -1);
let msg = c_ptr_to_string(err.message).unwrap();
assert_eq!(msg, "bad input");
error_clear(&mut err);
}
enum DomainError {
NotFound,
Io(String),
}
impl ErrorReport for DomainError {
fn code(&self) -> i32 {
match self {
DomainError::NotFound => 1001,
DomainError::Io(_) => 1004,
}
}
fn message(&self) -> String {
match self {
DomainError::NotFound => "not found".to_string(),
DomainError::Io(detail) => format!("io: {detail}"),
}
}
}
#[test]
fn error_report_blanket_display_uses_generic_code() {
let e = "boom".to_string();
assert_eq!(ErrorReport::code(&e), -1);
assert_eq!(ErrorReport::message(&e), "boom");
}
#[test]
fn error_report_domain_error_carries_its_code() {
let mut err = weaveffi_error::default();
let val: Result<i32, DomainError> = Err(DomainError::NotFound);
assert_eq!(result_to_out_err(val, &mut err), None);
assert_eq!(err.code, 1001);
assert_eq!(c_ptr_to_string(err.message).unwrap(), "not found");
error_clear(&mut err);
let val: Result<i32, DomainError> = Err(DomainError::Io("disk".to_string()));
assert_eq!(result_to_out_err(val, &mut err), None);
assert_eq!(err.code, 1004);
assert_eq!(c_ptr_to_string(err.message).unwrap(), "io: disk");
error_clear(&mut err);
}
#[test]
fn string_with_interior_nul_is_sanitized() {
let ptr = string_to_c_ptr("hel\0lo");
let recovered = c_ptr_to_string(ptr).unwrap();
assert_eq!(recovered, "hello");
free_string(ptr);
}
#[test]
fn c_ptr_to_string_null_returns_none() {
assert_eq!(c_ptr_to_string(ptr::null()), None);
}
#[test]
fn cancel_token_lifecycle() {
let token = cancel_token_create();
assert!(!token.is_null());
assert!(!cancel_token_is_cancelled(token));
cancel_token_cancel(token);
assert!(cancel_token_is_cancelled(token));
cancel_token_destroy(token);
}
#[test]
fn cancel_token_null_is_safe() {
cancel_token_cancel(ptr::null_mut());
assert!(!cancel_token_is_cancelled(ptr::null()));
cancel_token_destroy(ptr::null_mut());
}
}