use polyplug_abi::HostApi;
use polyplug_abi::runtime::RuntimeConfig;
use crate::runtime::Runtime;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn polyplug_runtime_create(config: *const RuntimeConfig) -> *const HostApi {
std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {
let mut builder = Runtime::builder();
if !config.is_null() {
let rt_config: &RuntimeConfig = unsafe { &*config };
builder = builder.config(rt_config.clone());
if let Some(cb) = rt_config.on_reload {
builder = builder.on_reload(move |user_data, phase| {
unsafe {
cb(
user_data,
&phase as *const polyplug_abi::runtime::ReloadPhase,
)
};
});
}
}
match builder.build() {
Ok(rt) => {
let host_abi: *const HostApi = rt.host_abi();
let runtime_ptr: *const Runtime = std::sync::Arc::into_raw(rt);
let stored_runtime: *const Runtime =
unsafe { (*host_abi).runtime as *const Runtime };
debug_assert_eq!(
stored_runtime, runtime_ptr,
"HostApi.runtime must point at the Arc target"
);
host_abi
}
Err(_) => core::ptr::null(),
}
}))
.unwrap_or(core::ptr::null())
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn polyplug_runtime_destroy(host: *const HostApi) {
std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {
if !host.is_null() {
let runtime_ptr: *const Runtime = unsafe { (*host).runtime as *const Runtime };
if !runtime_ptr.is_null() {
let _runtime: std::sync::Arc<Runtime> =
unsafe { std::sync::Arc::from_raw(runtime_ptr) };
}
}
}))
.unwrap_or(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
use polyplug_abi::AbiErrorCode;
#[test]
fn test_runtime_new_and_free() {
let host: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host.is_null());
unsafe { polyplug_runtime_destroy(host) };
}
#[test]
fn multiple_ffi_runtimes_are_isolated() {
let host1: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
let host2: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host1.is_null());
assert!(!host2.is_null());
assert_ne!(host1, host2);
let mut rc1: polyplug_abi::AbiError = polyplug_abi::AbiError::ok();
unsafe { ((*host1).load_bundle)(host1, core::ptr::null(), 0, &mut rc1) };
assert_eq!(rc1.code, AbiErrorCode::InvalidPointer as u32);
let len1: usize = unsafe { ((*host1).get_error_len)(host1) };
let len2: usize = unsafe { ((*host2).get_error_len)(host2) };
assert!(
len1 > 0,
"runtime 1 must have its own last_error after a failed load"
);
assert_eq!(
len2, 0,
"runtime 2 must NOT see runtime 1's error — state must be isolated"
);
let mut rc2: polyplug_abi::AbiError = polyplug_abi::AbiError::ok();
unsafe { ((*host2).load_bundle)(host2, core::ptr::null(), 0, &mut rc2) };
assert_eq!(rc2.code, AbiErrorCode::InvalidPointer as u32);
let len2_after: usize = unsafe { ((*host2).get_error_len)(host2) };
assert!(len2_after > 0, "runtime 2 now has its own last_error");
unsafe {
polyplug_runtime_destroy(host1);
polyplug_runtime_destroy(host2);
}
}
#[test]
fn multiple_ffi_runtimes_with_config() {
use polyplug_abi::runtime::Compatibility;
let config1: RuntimeConfig = RuntimeConfig {
compatibility: Compatibility::Strict,
hot_reload_enabled: true,
on_reload: None,
on_reload_user_data: core::ptr::null_mut(),
..Default::default()
};
let config2: RuntimeConfig = RuntimeConfig {
compatibility: Compatibility::Relaxed,
hot_reload_enabled: false,
on_reload: None,
on_reload_user_data: core::ptr::null_mut(),
..Default::default()
};
let host1: *const HostApi = unsafe { polyplug_runtime_create(&config1) };
let host2: *const HostApi = unsafe { polyplug_runtime_create(&config2) };
assert!(!host1.is_null());
assert!(!host2.is_null());
assert_ne!(host1, host2);
unsafe {
polyplug_runtime_destroy(host1);
polyplug_runtime_destroy(host2);
}
}
#[test]
fn host_interface_load_bundle_returns_error_on_null() {
let host: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host.is_null());
let mut result: polyplug_abi::AbiError = polyplug_abi::AbiError::ok();
unsafe { ((*host).load_bundle)(host, core::ptr::null(), 0, &mut result) };
assert_eq!(result.code, AbiErrorCode::InvalidPointer as u32);
unsafe { polyplug_runtime_destroy(host) };
}
#[test]
fn host_interface_find_guest_contract_returns_null_on_empty_registry() {
let host: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host.is_null());
let handle = unsafe { ((*host).find_guest_contract)(host, 12345, 0) };
assert!(handle.is_null());
unsafe { polyplug_runtime_destroy(host) };
}
#[test]
fn host_interface_get_error_len_on_clean_runtime() {
let host: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host.is_null());
let len = unsafe { ((*host).get_error_len)(host) };
assert_eq!(len, 0);
unsafe { polyplug_runtime_destroy(host) };
}
#[test]
fn multiple_ffi_runtimes_concurrent_operations() {
use core::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
let success_count: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let handles: Vec<thread::JoinHandle<()>> = (0..4)
.map(|_| {
let success: Arc<AtomicUsize> = Arc::clone(&success_count);
thread::spawn(move || {
for _ in 0..10 {
let host: *const HostApi =
unsafe { polyplug_runtime_create(core::ptr::null()) };
if !host.is_null() {
success.fetch_add(1, Ordering::SeqCst);
unsafe { polyplug_runtime_destroy(host) };
}
}
})
})
.collect();
for h in handles {
h.join().expect("thread should not panic");
}
assert_eq!(success_count.load(Ordering::SeqCst), 40);
}
#[test]
fn multiple_ffi_runtimes_lifecycle_interleaved() {
let host1: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host1.is_null());
unsafe { polyplug_runtime_destroy(host1) };
let host2: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host2.is_null());
unsafe { polyplug_runtime_destroy(host2) };
let host3: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host3.is_null());
unsafe { polyplug_runtime_destroy(host3) };
}
#[test]
fn ffi_runtime_create_with_null_options() {
let host: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host.is_null());
unsafe { polyplug_runtime_destroy(host) };
}
#[test]
fn ffi_runtime_destroy_null_is_safe() {
unsafe { polyplug_runtime_destroy(core::ptr::null()) };
}
#[test]
fn multiple_ffi_runtimes_parallel_mixed_ops() {
use core::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
let success_count: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let error_count: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let handles: Vec<thread::JoinHandle<()>> = (0..8)
.map(|_| {
let success: Arc<AtomicUsize> = Arc::clone(&success_count);
let errors: Arc<AtomicUsize> = Arc::clone(&error_count);
thread::spawn(move || {
let host: *const HostApi =
unsafe { polyplug_runtime_create(core::ptr::null()) };
if host.is_null() {
return;
}
let mut result: polyplug_abi::AbiError = polyplug_abi::AbiError::ok();
unsafe { ((*host).load_bundle)(host, b"/bad".as_ptr(), 4, &mut result) };
if result.code == AbiErrorCode::Ok as u32 {
success.fetch_add(1, Ordering::SeqCst);
} else {
errors.fetch_add(1, Ordering::SeqCst);
}
unsafe { polyplug_runtime_destroy(host) };
})
})
.collect();
for h in handles {
h.join().expect("thread should not panic");
}
assert_eq!(
success_count.load(Ordering::SeqCst) + error_count.load(Ordering::SeqCst),
8
);
}
#[test]
fn host_interface_resolve_guest_contract_returns_null_on_null_handle() {
let host: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host.is_null());
let null_handle = polyplug_abi::GuestContractHandle::null();
let interface = unsafe { ((*host).resolve_guest_contract)(host, null_handle) };
assert!(interface.is_null());
unsafe { polyplug_runtime_destroy(host) };
}
#[test]
fn host_interface_has_runtime_pointer() {
let host: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host.is_null());
let runtime_ptr = unsafe { (*host).runtime };
assert!(!runtime_ptr.is_null());
unsafe { polyplug_runtime_destroy(host) };
}
#[test]
fn host_interface_has_all_operation_fields() {
let host: *const HostApi = unsafe { polyplug_runtime_create(core::ptr::null()) };
assert!(!host.is_null());
let iface = unsafe { &*host };
let ptr = iface.register_guest_contract as *const ();
assert!(!ptr.is_null());
let ptr = iface.alloc as *const ();
assert!(!ptr.is_null());
let ptr = iface.free as *const ();
assert!(!ptr.is_null());
let ptr = iface.find_guest_contract as *const ();
assert!(!ptr.is_null());
let ptr = iface.find_all_guest_contracts as *const ();
assert!(!ptr.is_null());
let ptr = iface.resolve_guest_contract as *const ();
assert!(!ptr.is_null());
let ptr = iface.get_host_contract as *const ();
assert!(!ptr.is_null());
let ptr = iface.resolve_host_contract_interface as *const ();
assert!(!ptr.is_null());
let ptr = iface.list_bundles as *const ();
assert!(!ptr.is_null());
let ptr = iface.get_dependencies as *const ();
assert!(!ptr.is_null());
let ptr = iface.load_bundle as *const ();
assert!(!ptr.is_null());
let ptr = iface.reload_bundle as *const ();
assert!(!ptr.is_null());
let ptr = iface.register_host_contract as *const ();
assert!(!ptr.is_null());
let ptr = iface.register_loader as *const ();
assert!(!ptr.is_null());
let ptr = iface.get_last_error as *const ();
assert!(!ptr.is_null());
let ptr = iface.get_error_len as *const ();
assert!(!ptr.is_null());
unsafe { polyplug_runtime_destroy(host) };
}
}