#![allow(clippy::expect_used)]
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Output;
use polyplug::runtime_store::RuntimeStore;
use polyplug_abi::AbiError;
use polyplug_abi::AbiErrorCode;
use polyplug_abi::BundleInitContext;
use polyplug_abi::GuestContractHandle;
use polyplug_abi::GuestContractInstance;
use polyplug_abi::GuestContractInterface;
use polyplug_abi::HostApi;
use polyplug_abi::PluginDescriptor;
use polyplug_abi::ffi::polyplug_host_alloc;
use polyplug_abi::ffi::polyplug_host_free;
use polyplug_abi::types::StringView;
use polyplug_abi::types::abi_error_ok;
mod common;
use common::polyplugc_bin;
const TEST_PLUGIN_CPP_SO: &str = env!("TEST_PLUGIN_CPP_SO");
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("parent of crates/polyplug")
.parent()
.expect("workspace root")
.to_path_buf()
}
fn run_polyplugc_cpp(api_toml: &Path, out_dir: &Path) -> Output {
Command::new(polyplugc_bin())
.arg("generate")
.arg("--api")
.arg(api_toml)
.arg("--lang")
.arg("cpp")
.arg("--out")
.arg(out_dir)
.output()
.expect("failed to spawn polyplugc")
}
unsafe extern "C" fn registry_register_callback(
_this: *const HostApi,
descriptor: *const PluginDescriptor,
interface: *const GuestContractInterface,
out_err: *mut AbiError,
) {
if descriptor.is_null() || interface.is_null() {
if !out_err.is_null() {
unsafe {
out_err.write(AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
})
};
}
return;
}
let desc: &PluginDescriptor = unsafe { &*descriptor };
let vt: &GuestContractInterface = unsafe { &*interface };
let contract_name: &str = unsafe {
let bytes: &[u8] =
core::slice::from_raw_parts(desc.contract_name.ptr, desc.contract_name.len);
core::str::from_utf8_unchecked(bytes) };
let result: Result<GuestContractHandle, _> = CPP_DISPATCH_REGISTRY.with(|reg_cell| {
let registry: core::cell::Ref<'_, RuntimeStore> = reg_cell.borrow();
unsafe {
registry.register_guest_contract(
*desc,
interface,
contract_name.to_owned(),
polyplug_utils::BundleId::from_u64(vt.contract_id.id()),
)
}
});
let err: AbiError = match result {
Ok(_) => abi_error_ok(),
Err(_) => AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
},
};
if !out_err.is_null() {
unsafe { out_err.write(err) };
}
}
unsafe extern "C" fn noop_alloc(_this: *const HostApi, size: usize, align: usize) -> *mut u8 {
polyplug_host_alloc(size, align)
}
unsafe extern "C" fn noop_free(_this: *const HostApi, ptr: *mut u8, size: usize, align: usize) {
unsafe { polyplug_host_free(ptr, size, align) }
}
unsafe extern "C" fn noop_find_guest_contract(
_this: *const HostApi,
_contract_id: u64,
_min_version: u32,
) -> GuestContractHandle {
GuestContractHandle::null()
}
unsafe extern "C" fn noop_find_all_guest_contracts(
_this: *const HostApi,
_contract_id: u64,
_min_version: u32,
) -> polyplug_abi::Array<GuestContractHandle> {
polyplug_abi::Array::empty()
}
unsafe extern "C" fn noop_resolve_guest_contract(
_this: *const HostApi,
_handle: GuestContractHandle,
) -> *const GuestContractInterface {
core::ptr::null()
}
unsafe extern "C" fn noop_get_host_contract(
_this: *const HostApi,
_contract_id: u64,
_min_version: u32,
) -> polyplug_abi::HostContractInstance {
polyplug_abi::HostContractInstance::null()
}
unsafe extern "C" fn noop_list_bundles(
_this: *const HostApi,
) -> polyplug_abi::Array<polyplug_utils::BundleId> {
polyplug_abi::Array::empty()
}
unsafe extern "C" fn noop_get_dependencies(
_this: *const HostApi,
) -> polyplug_abi::Array<polyplug_abi::DependencyInfo> {
polyplug_abi::Array::empty()
}
unsafe extern "C" fn noop_resolve_host_contract_interface(
_this: *const HostApi,
_contract_id: u64,
_min_version: u32,
) -> *const polyplug_abi::HostContractInterface {
core::ptr::null()
}
unsafe extern "C" fn noop_load_bundle(
_this: *const HostApi,
_path: *const u8,
_path_len: usize,
out_err: *mut AbiError,
) {
if !out_err.is_null() {
unsafe { out_err.write(AbiError::ok()) };
}
}
unsafe extern "C" fn noop_reload_bundle(
_this: *const HostApi,
_path: *const u8,
_path_len: usize,
out_err: *mut AbiError,
) {
if !out_err.is_null() {
unsafe { out_err.write(AbiError::ok()) };
}
}
unsafe extern "C" fn noop_register_host_contract(
_this: *const HostApi,
_interface: *const polyplug_abi::HostContractInterface,
out_err: *mut AbiError,
) {
if !out_err.is_null() {
unsafe { out_err.write(AbiError::ok()) };
}
}
unsafe extern "C" fn noop_register_loader(
_this: *const HostApi,
_loader_ptr: *mut core::ffi::c_void,
out_err: *mut AbiError,
) {
if !out_err.is_null() {
unsafe { out_err.write(AbiError::ok()) };
}
}
unsafe extern "C" fn noop_get_last_error(
_this: *const HostApi,
_buf: *mut u8,
_buf_len: usize,
) -> usize {
0
}
unsafe extern "C" fn noop_get_error_len(_this: *const HostApi) -> usize {
0
}
unsafe extern "C" fn noop_unload_bundle(
_this: *const HostApi,
_bundle_id: polyplug_utils::BundleId,
out_err: *mut AbiError,
) {
if !out_err.is_null() {
unsafe { out_err.write(AbiError::ok()) };
}
}
fn make_host_interface() -> HostApi {
HostApi {
runtime: core::ptr::null_mut(),
register_guest_contract: registry_register_callback,
alloc: noop_alloc,
free: noop_free,
find_guest_contract: noop_find_guest_contract,
find_all_guest_contracts: noop_find_all_guest_contracts,
resolve_guest_contract: noop_resolve_guest_contract,
get_host_contract: noop_get_host_contract,
resolve_host_contract_interface: noop_resolve_host_contract_interface,
list_bundles: noop_list_bundles,
get_dependencies: noop_get_dependencies,
load_bundle: noop_load_bundle,
reload_bundle: noop_reload_bundle,
register_host_contract: noop_register_host_contract,
register_loader: noop_register_loader,
get_last_error: noop_get_last_error,
get_error_len: noop_get_error_len,
unload_bundle: noop_unload_bundle,
log: stub_host_log,
create_guest_instance: stub_create_guest_instance,
destroy_guest_instance: stub_destroy_guest_instance,
revision_counter: stub_revision_counter,
reserved: core::ptr::null(),
}
}
std::thread_local! {
static CPP_DISPATCH_REGISTRY: core::cell::RefCell<RuntimeStore> =
core::cell::RefCell::new(RuntimeStore::new());
}
#[repr(C)]
struct AddArgs {
a: u32,
b: u32,
}
#[test]
fn test_cpp_codegen_files_exist() {
let out_dir: PathBuf = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("gen_cpp_codegen");
let bundle_toml: PathBuf = workspace_root()
.join("tests")
.join("fixtures")
.join("test_bundle.toml");
std::fs::create_dir_all(&out_dir).expect("failed to create out_dir");
let gen_output: Output = Command::new(polyplugc_bin())
.arg("generate")
.arg("--bundle")
.arg(&bundle_toml)
.arg("--lang")
.arg("cpp")
.arg("--out")
.arg(&out_dir)
.output()
.expect("failed to spawn polyplugc");
assert!(
gen_output.status.success(),
"polyplugc generate --lang cpp failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&gen_output.stdout),
String::from_utf8_lossy(&gen_output.stderr),
);
let expected_files: [&str; 5] = [
"guest/types.hpp",
"guest/contracts.hpp",
"guest/interfaces.hpp",
"guest/init.hpp",
"manifest.toml",
];
for filename in expected_files {
let file_path: PathBuf = out_dir.join(filename);
assert!(
file_path.exists(),
"expected generated file not found: {}",
file_path.display()
);
}
println!(
"test_cpp_codegen_files_exist: all 5 guest files present in {} ✓",
out_dir.display()
);
let gpp_version_result: std::io::Result<std::process::Output> =
Command::new("g++").args(["--version"]).output();
if let Ok(version_out) = gpp_version_result {
if version_out.status.success() {
let sdks_cpp_abi: PathBuf = workspace_root().join("sdks").join("cpp").join("abi");
let interfaces_hpp: PathBuf = out_dir.join("guest").join("interfaces.hpp");
let out_obj: PathBuf =
PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("test_cpp_codegen_interfaces.o");
let compile_result: std::process::Output = Command::new("g++")
.arg("-std=c++20")
.arg(format!("-I{}", out_dir.join("guest").display()))
.arg(format!("-I{}", out_dir.join("host").display()))
.arg(format!("-I{}", sdks_cpp_abi.display()))
.arg(&interfaces_hpp)
.arg("-c")
.arg("-o")
.arg(&out_obj)
.output()
.expect("g++ failed to run");
assert!(
compile_result.status.success(),
"interfaces.hpp did not compile:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&compile_result.stdout),
String::from_utf8_lossy(&compile_result.stderr),
);
println!("test_cpp_codegen_files_exist: interfaces.hpp compiled successfully ✓");
} else {
eprintln!("skipping g++ compile check: g++ --version returned non-zero");
}
} else {
eprintln!("skipping g++ compile check: g++ not found");
}
}
fn test_add_contract_id() -> polyplug_utils::GuestContractId {
polyplug_utils::GuestContractId::new("test.add", 1)
}
#[test]
fn test_cpp_plugin_dispatch() {
if TEST_PLUGIN_CPP_SO.is_empty() {
eprintln!("skipping cpp dispatch test: TEST_PLUGIN_CPP_SO not set (g++ not available)");
return;
}
let library: libloading::Library = unsafe {
libloading::Library::new(TEST_PLUGIN_CPP_SO)
.expect("failed to load C++ test plugin shared library")
};
let init_fn: libloading::Symbol<
'_,
unsafe extern "C" fn(*const HostApi, *const BundleInitContext) -> AbiError,
> = unsafe {
library
.get(b"polyplug_init\0")
.expect("polyplug_init symbol not found in C++ plugin")
};
CPP_DISPATCH_REGISTRY.with(|cell| {
*cell.borrow_mut() = RuntimeStore::new();
});
let host_interface: HostApi = make_host_interface();
let ctx: BundleInitContext = BundleInitContext {
bundle_id: 0,
bundle_path: StringView::null(),
};
let init_result: AbiError = unsafe {
init_fn(
&host_interface as *const HostApi,
&ctx as *const BundleInitContext,
)
};
assert_eq!(
init_result.code,
AbiErrorCode::Ok as u32,
"polyplug_init must return Ok"
);
let handle: GuestContractHandle = CPP_DISPATCH_REGISTRY.with(|cell| {
cell.borrow()
.find(test_add_contract_id(), 0_u32)
.expect("test.add must be registered after polyplug_init")
});
let interface_ptr: *const GuestContractInterface = CPP_DISPATCH_REGISTRY.with(|cell| {
cell.borrow()
.resolve_guest_contract(handle)
.expect("interface must be resolvable from handle")
});
let interface: &GuestContractInterface = unsafe { &*interface_ptr };
let fn_ptr: *const () = unsafe { *interface.dispatch.native.functions.add(0) };
let dispatch_fn: unsafe extern "C" fn(
GuestContractInstance,
*const (),
*mut (),
*mut AbiError,
) = unsafe { core::mem::transmute(fn_ptr) };
let args: AddArgs = AddArgs {
a: 10_u32,
b: 20_u32,
};
let mut out: u32 = 0_u32;
let mut call_result: AbiError = AbiError::ok();
unsafe {
dispatch_fn(
GuestContractInstance::null(),
&args as *const AddArgs as *const (),
&mut out as *mut u32 as *mut (),
&mut call_result,
)
};
assert_eq!(
call_result.code,
AbiErrorCode::Ok as u32,
"cpp_test_add must return Ok"
);
assert_eq!(out, 30_u32, "add(10, 20) must equal 30");
println!("test_cpp_plugin_dispatch: add(10, 20) = {} ✓", out);
core::mem::forget(library);
}
const TEST_PLUGIN_SO: &str = env!("TEST_PLUGIN_SO");
#[test]
fn test_cpp_host_loads_rust_plugin() {
if TEST_PLUGIN_SO.is_empty() {
eprintln!("skipping: TEST_PLUGIN_SO not set");
return;
}
let library: libloading::Library = unsafe {
libloading::Library::new(TEST_PLUGIN_SO).expect("failed to load Rust test plugin")
};
let init_fn: libloading::Symbol<
'_,
unsafe extern "C" fn(*const HostApi, *const BundleInitContext) -> AbiError,
> = unsafe {
library
.get(b"polyplug_init\0")
.expect("polyplug_init not found in Rust plugin")
};
CPP_DISPATCH_REGISTRY.with(|cell| {
*cell.borrow_mut() = RuntimeStore::new();
});
let host_interface: HostApi = make_host_interface();
let ctx: BundleInitContext = BundleInitContext {
bundle_id: 0,
bundle_path: StringView::null(),
};
let init_result: AbiError = unsafe {
init_fn(
&host_interface as *const HostApi,
&ctx as *const BundleInitContext,
)
};
assert_eq!(
init_result.code,
AbiErrorCode::Ok as u32,
"Rust plugin polyplug_init must return Ok"
);
let handle: GuestContractHandle = CPP_DISPATCH_REGISTRY.with(|cell| {
cell.borrow()
.find(test_add_contract_id(), 0_u32)
.expect("test.add must be registered from Rust plugin")
});
let interface_ptr: *const GuestContractInterface = CPP_DISPATCH_REGISTRY.with(|cell| {
cell.borrow()
.resolve_guest_contract(handle)
.expect("interface must be resolvable")
});
let interface: &GuestContractInterface = unsafe { &*interface_ptr };
let args: AddArgs = AddArgs { a: 3_u32, b: 5_u32 };
let mut out: u32 = 0_u32;
let fn_ptr: *const () = unsafe { *interface.dispatch.native.functions.add(0) };
let dispatch_fn: unsafe extern "C" fn(
GuestContractInstance,
*const (),
*mut (),
*mut AbiError,
) = unsafe { core::mem::transmute(fn_ptr) };
let mut call_result: AbiError = AbiError::ok();
unsafe {
dispatch_fn(
GuestContractInstance::null(),
&args as *const AddArgs as *const (),
&mut out as *mut u32 as *mut (),
&mut call_result,
)
};
assert_eq!(
call_result.code,
AbiErrorCode::Ok as u32,
"Rust plugin add(3,5) must return Ok"
);
assert_eq!(out, 8_u32, "Rust plugin add(3,5) must equal 8");
println!(
"test_cpp_host_loads_rust_plugin: Rust plugin add(3,5) = {} ✓",
out
);
core::mem::forget(library);
}
const TEST_PLUGIN_CPP_THROW_SO: &str = env!("TEST_PLUGIN_CPP_THROW_SO");
#[test]
fn test_exception_isolation_cpp() {
if TEST_PLUGIN_CPP_THROW_SO.is_empty() {
eprintln!("skipping exception isolation test: g++ not available");
return;
}
let library: libloading::Library = unsafe {
libloading::Library::new(TEST_PLUGIN_CPP_THROW_SO)
.expect("failed to load throwing C++ test plugin")
};
let init_fn: libloading::Symbol<
'_,
unsafe extern "C" fn(*const HostApi, *const BundleInitContext) -> AbiError,
> = unsafe {
library
.get(b"polyplug_init\0")
.expect("polyplug_init not found")
};
CPP_DISPATCH_REGISTRY.with(|cell| {
*cell.borrow_mut() = RuntimeStore::new();
});
let host_interface: HostApi = make_host_interface();
let ctx: BundleInitContext = BundleInitContext {
bundle_id: 0,
bundle_path: StringView::null(),
};
let init_result: AbiError = unsafe {
init_fn(
&host_interface as *const HostApi,
&ctx as *const BundleInitContext,
)
};
assert_eq!(
init_result.code,
AbiErrorCode::Ok as u32,
"throwing plugin init must return Ok"
);
let handle: GuestContractHandle = CPP_DISPATCH_REGISTRY.with(|cell| {
cell.borrow()
.find(test_add_contract_id(), 0_u32)
.expect("test.add registered from throwing plugin")
});
let interface_ptr: *const GuestContractInterface = CPP_DISPATCH_REGISTRY.with(|cell| {
cell.borrow()
.resolve_guest_contract(handle)
.expect("interface resolvable")
});
let interface: &GuestContractInterface = unsafe { &*interface_ptr };
let args: AddArgs = AddArgs { a: 0_u32, b: 0_u32 };
let mut out: u32 = 0_u32;
let fn_ptr: *const () = unsafe { *interface.dispatch.native.functions.add(0) };
let dispatch_fn: unsafe extern "C" fn(
GuestContractInstance,
*const (),
*mut (),
*mut AbiError,
) = unsafe { core::mem::transmute(fn_ptr) };
let mut call_result: AbiError = AbiError::ok();
unsafe {
dispatch_fn(
GuestContractInstance::null(),
&args as *const AddArgs as *const (),
&mut out as *mut u32 as *mut (),
&mut call_result,
)
};
assert_eq!(
call_result.code,
AbiErrorCode::Generic as u32,
"exception must be caught and returned as Generic"
);
println!("test_exception_isolation_cpp: exception caught, host survived ✓");
core::mem::forget(library);
}
#[test]
fn test_cpp_codegen_generates_enum_types() {
let out_dir: PathBuf =
PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("integration_codegen_cpp_enum");
let api_toml: PathBuf = workspace_root()
.join("tests")
.join("fixtures")
.join("test_api.toml");
std::fs::create_dir_all(&out_dir).expect("failed to create out_dir");
let gen_output: Output = run_polyplugc_cpp(&api_toml, &out_dir);
assert!(
gen_output.status.success(),
"polyplugc generate --lang cpp failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&gen_output.stdout),
String::from_utf8_lossy(&gen_output.stderr),
);
let types_file: PathBuf = out_dir.join("host").join("types.hpp");
let content: String = std::fs::read_to_string(&types_file).expect("read types file");
assert!(
content.contains("enum class PixelFormat"),
"types.hpp must contain enum class PixelFormat"
);
assert!(
content.contains("operator|"),
"types.hpp must contain operator|"
);
println!("test_cpp_codegen_generates_enum_types: all enum assertions passed ✓");
}
unsafe extern "C" fn stub_host_log(
_this: *const polyplug_abi::HostApi,
_level: u32,
_scope: polyplug_abi::StringView,
_message: polyplug_abi::StringView,
) {
}
unsafe extern "C" fn stub_create_guest_instance(
_this: *const polyplug_abi::HostApi,
_interface: *const polyplug_abi::GuestContractInterface,
_args: *const core::ffi::c_void,
out_instance: *mut polyplug_abi::GuestContractInstance,
) {
if !out_instance.is_null() {
unsafe { out_instance.write(polyplug_abi::GuestContractInstance::null()) };
}
}
unsafe extern "C" fn stub_destroy_guest_instance(
_this: *const polyplug_abi::HostApi,
_interface: *const polyplug_abi::GuestContractInterface,
_instance: polyplug_abi::GuestContractInstance,
) {
}
unsafe extern "C" fn stub_revision_counter(_this: *const polyplug_abi::HostApi) -> *const u64 {
core::ptr::null()
}