#![allow(clippy::expect_used)]
use core::ffi::c_void;
use polyplug_abi::AbiError;
use polyplug_abi::AbiErrorCode;
use polyplug_abi::Array;
use polyplug_abi::BundleInitContext;
use polyplug_abi::DependencyInfo;
use polyplug_abi::GuestContractHandle;
use polyplug_abi::GuestContractInstance;
use polyplug_abi::GuestContractInterface;
use polyplug_abi::HostApi;
use polyplug_abi::PluginDescriptor;
use polyplug_abi::StringView;
use polyplug_utils::BundleId;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Output;
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("parent of crates/polyplug")
.parent()
.expect("workspace root")
.to_path_buf()
}
fn so_filename() -> &'static str {
if cfg!(target_os = "macos") {
"libsmoke_rust_test_plugin.dylib"
} else if cfg!(target_os = "windows") {
"smoke_rust_test_plugin.dll"
} else {
"libsmoke_rust_test_plugin.so"
}
}
fn run_polyplugc_rust(bundle_toml: &Path, out_dir: &Path) -> Output {
Command::new(env!("CARGO_BIN_EXE_polyplugc"))
.arg("generate")
.arg("--bundle")
.arg(bundle_toml)
.arg("--lang")
.arg("rust")
.arg("--out")
.arg(out_dir)
.output()
.expect("failed to spawn polyplugc")
}
fn run_polyplugc_cpp(bundle_toml: &Path, out_dir: &Path) -> Output {
Command::new(env!("CARGO_BIN_EXE_polyplugc"))
.arg("generate")
.arg("--bundle")
.arg(bundle_toml)
.arg("--lang")
.arg("cpp")
.arg("--out")
.arg(out_dir)
.output()
.expect("failed to spawn polyplugc")
}
fn write_plugin_cargo_toml(crate_dir: &Path, guest_lib_path: &Path) {
let abi_lib_path: PathBuf = workspace_root().join("crates").join("polyplug_abi");
let utils_lib_path: PathBuf = workspace_root().join("crates").join("polyplug_utils");
let content: String = format!(
r#"[package]
name = "smoke_rust_test_plugin"
version = "0.1.0"
edition = "2021"
[lib]
name = "smoke_rust_test_plugin"
crate-type = ["cdylib"]
[dependencies]
polyplug_abi = {{ path = "{}" }}
polyplug_guest = {{ path = "{}" }}
polyplug_utils = {{ path = "{}" }}
[workspace]
"#,
abi_lib_path.to_string_lossy().replace('\\', "/"),
guest_lib_path.to_string_lossy().replace('\\', "/"),
utils_lib_path.to_string_lossy().replace('\\', "/")
);
let cargo_toml_path: PathBuf = crate_dir.join("Cargo.toml");
std::fs::write(&cargo_toml_path, content).expect("failed to write plugin Cargo.toml");
}
fn write_plugin_lib_rs(src_dir: &Path) {
let content: &str = r#"// THIS FILE IS WRITTEN BY smoke TEST — DO NOT EDIT BY HAND
mod guest {
pub mod types;
pub mod contracts;
pub mod interfaces;
}
#[allow(unused_imports)]
use polyplug_abi::AbiErrorCode;
use polyplug_abi::AbiError;
use polyplug_abi::PluginDescriptor;
use polyplug_guest::GuestError;
use polyplug_abi::HostApi;
use polyplug_abi::BundleInitContext;
use polyplug_abi::StringView;
use polyplug_abi::Version;
use guest::contracts::TestAddGuestContract;
use guest::types::AddArgs;
use guest::interfaces::TEST_ADDER_INTERFACE;
use polyplug_guest::HostContext;
struct MyPlugin;
impl TestAddGuestContract for MyPlugin {
fn add(&self, args: &AddArgs) -> Result<u32, GuestError> {
Ok(args.a.wrapping_add(args.b))
}
fn add_primitive(&self, a: u32, b: u32) -> Result<u32, GuestError> {
Ok(a.wrapping_add(b))
}
fn version(&self) -> Result<StringView, GuestError> {
Ok(StringView { ptr: b"1.0.0".as_ptr(), len: 5_usize })
}
fn reset(&self) -> Result<(), GuestError> {
Ok(())
}
}
/// Factory called by the generated `create_instance` for every host-created
/// instance. The implementation travels in `GuestContractInstance.data` —
/// no static storage.
#[no_mangle]
pub fn polyplug_create_test_adder(_host: HostContext) -> Box<dyn TestAddGuestContract> {
Box::new(MyPlugin)
}
/// # Safety
/// `host` must be a valid non-null pointer provided by the host.
#[no_mangle]
pub unsafe extern "C" fn polyplug_init(
host: *const HostApi,
_ctx: *const BundleInitContext,
) -> AbiError {
if host.is_null() {
return AbiError { code: AbiErrorCode::Generic as u32, message: StringView::null() };
}
// SAFETY: host is non-null and valid per ABI contract.
let host: &HostApi = unsafe { &*host };
let desc: PluginDescriptor = PluginDescriptor {
name: StringView { ptr: b"smoke_test_plugin".as_ptr(), len: 17_usize },
contract_name: StringView { ptr: b"test.add".as_ptr(), len: 8_usize },
version: Version { major: 1, minor: 0, patch: 0 },
};
// Out-param ABI: register_guest_contract writes its AbiError through a
// trailing pointer and returns void; init surfaces it by value.
let mut err: AbiError = AbiError::ok();
// SAFETY: desc and TEST_ADDER_INTERFACE are 'static; host is valid; &mut err is valid.
unsafe {
(host.register_guest_contract)(
host as *const _,
&desc as *const PluginDescriptor,
&TEST_ADDER_INTERFACE as *const _,
&mut err as *mut AbiError,
)
};
err
}
"#;
let lib_rs_path: PathBuf = src_dir.join("lib.rs");
std::fs::write(&lib_rs_path, content).expect("failed to write plugin src/lib.rs");
}
std::thread_local! {
static CAPTURED_INTERFACE: core::cell::Cell<*const GuestContractInterface> =
const { core::cell::Cell::new(core::ptr::null()) };
}
unsafe extern "C" fn capture_interface_callback(
_host: *const HostApi,
_descriptor: *const PluginDescriptor,
interface: *const GuestContractInterface,
out_err: *mut AbiError,
) {
CAPTURED_INTERFACE.with(|cell| cell.set(interface));
if !out_err.is_null() {
unsafe { out_err.write(AbiError::ok()) };
}
}
#[repr(C)]
struct AddArgs {
a: u32,
b: u32,
}
unsafe extern "C" fn stub_alloc(_host: *const HostApi, size: usize, align: usize) -> *mut u8 {
polyplug_abi::ffi::polyplug_host_alloc(size, align)
}
unsafe extern "C" fn stub_free(_host: *const HostApi, ptr: *mut u8, size: usize, align: usize) {
unsafe {
polyplug_abi::ffi::polyplug_host_free(ptr, size, align);
}
}
unsafe extern "C" fn stub_find_guest_contract(
_host: *const HostApi,
_contract_id: u64,
_min_version: u32,
) -> GuestContractHandle {
GuestContractHandle {
index: u32::MAX,
generation: 0,
}
}
unsafe extern "C" fn stub_find_all_guest_contracts(
_host: *const HostApi,
_contract_id: u64,
_min_version: u32,
) -> Array<GuestContractHandle> {
Array::empty()
}
unsafe extern "C" fn stub_resolve_guest_contract(
_host: *const HostApi,
_handle: GuestContractHandle,
) -> *const GuestContractInterface {
core::ptr::null()
}
unsafe extern "C" fn stub_get_host_contract(
_host: *const HostApi,
_contract_id: u64,
_min_version: u32,
) -> polyplug_abi::HostContractInstance {
polyplug_abi::HostContractInstance {
data: core::ptr::null_mut(),
}
}
unsafe extern "C" fn stub_resolve_host_contract_interface(
_host: *const HostApi,
_contract_id: u64,
_min_version: u32,
) -> *const polyplug_abi::HostContractInterface {
core::ptr::null()
}
unsafe extern "C" fn stub_list_bundles(_host: *const HostApi) -> Array<BundleId> {
Array::empty()
}
unsafe extern "C" fn stub_get_dependencies(_host: *const HostApi) -> Array<DependencyInfo> {
Array::empty()
}
unsafe extern "C" fn stub_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 stub_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 stub_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 stub_register_loader(
_this: *const HostApi,
_loader_ptr: *mut c_void,
out_err: *mut AbiError,
) {
if !out_err.is_null() {
unsafe { out_err.write(AbiError::ok()) };
}
}
unsafe extern "C" fn stub_get_last_error(
_this: *const HostApi,
_buf: *mut u8,
_buf_len: usize,
) -> usize {
0
}
unsafe extern "C" fn stub_get_error_len(_this: *const HostApi) -> usize {
0
}
unsafe extern "C" fn stub_unload_bundle(
_this: *const HostApi,
_bundle_id: BundleId,
out_err: *mut AbiError,
) {
if !out_err.is_null() {
unsafe { out_err.write(AbiError::ok()) };
}
}
#[test]
fn smoke_rust_codegen_dispatch() {
let tmp_dir: PathBuf = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("smoke_rust_test");
let src_dir: PathBuf = tmp_dir.join("src");
let bundle_toml: PathBuf = workspace_root()
.join("tests")
.join("fixtures")
.join("test_bundle.toml");
let guest_lib_path: PathBuf = workspace_root().join("sdks").join("rust").join("guest");
std::fs::create_dir_all(&src_dir).expect("failed to create src dir");
let gen_output: Output = run_polyplugc_rust(&bundle_toml, &src_dir);
assert!(
gen_output.status.success(),
"polyplugc generate --lang rust failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&gen_output.stdout),
String::from_utf8_lossy(&gen_output.stderr),
);
write_plugin_cargo_toml(&tmp_dir, &guest_lib_path);
write_plugin_lib_rs(&src_dir);
let workspace_root_path: PathBuf = workspace_root();
let target_dir: PathBuf = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("smoke_rust_build");
let build_status: std::process::ExitStatus = Command::new(env!("CARGO"))
.arg("build")
.arg("--release")
.arg("--target-dir")
.arg(&target_dir)
.arg("--manifest-path")
.arg(tmp_dir.join("Cargo.toml"))
.current_dir(&workspace_root_path)
.status()
.expect("failed to spawn cargo build");
assert!(
build_status.success(),
"cargo build of generated smoke Rust plugin failed"
);
let so_path: PathBuf = target_dir.join("release").join(so_filename());
assert!(
so_path.exists(),
"compiled .so not found at {}",
so_path.display()
);
let library: libloading::Library =
unsafe { libloading::Library::new(&so_path).expect("failed to load smoke plugin .so") };
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")
};
CAPTURED_INTERFACE.with(|cell| cell.set(core::ptr::null()));
let host_abi: HostApi = HostApi {
runtime: core::ptr::null_mut(),
register_guest_contract: capture_interface_callback,
alloc: stub_alloc,
free: stub_free,
find_guest_contract: stub_find_guest_contract,
find_all_guest_contracts: stub_find_all_guest_contracts,
resolve_guest_contract: stub_resolve_guest_contract,
get_host_contract: stub_get_host_contract,
resolve_host_contract_interface: stub_resolve_host_contract_interface,
list_bundles: stub_list_bundles,
get_dependencies: stub_get_dependencies,
load_bundle: stub_load_bundle,
reload_bundle: stub_reload_bundle,
register_host_contract: stub_register_host_contract,
register_loader: stub_register_loader,
get_last_error: stub_get_last_error,
get_error_len: stub_get_error_len,
unload_bundle: stub_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(),
};
let ctx: BundleInitContext = BundleInitContext {
bundle_id: 0,
bundle_path: StringView::null(),
};
let init_result: AbiError = unsafe {
init_fn(
&host_abi as *const HostApi,
&ctx as *const BundleInitContext,
)
};
assert_eq!(
init_result.code,
AbiErrorCode::Ok as u32,
"polyplug_init must return Ok"
);
let interface_ptr: *const GuestContractInterface = CAPTURED_INTERFACE.with(|cell| cell.get());
assert!(
!interface_ptr.is_null(),
"interface pointer must be non-null after polyplug_init"
);
let interface: &GuestContractInterface = unsafe { &*interface_ptr };
let function_count: u32 = unsafe { interface.dispatch.native.function_count };
assert_eq!(
function_count, 4_u32,
"test.add interface must have 4 functions"
);
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 instance: GuestContractInstance = GuestContractInstance::null();
unsafe {
(interface.create_instance)(
polyplug_abi::VmLoaderData::null(),
&host_abi as *const HostApi,
core::ptr::null(),
&mut instance as *mut GuestContractInstance,
)
};
assert!(
!instance.data.is_null(),
"create_instance must produce a non-null instance payload"
);
let mut call_result: AbiError = AbiError::ok();
unsafe {
dispatch_fn(
instance,
&args as *const AddArgs as *const (),
&mut out as *mut u32 as *mut (),
&mut call_result as *mut AbiError,
)
};
unsafe {
(interface.destroy_instance)(
polyplug_abi::VmLoaderData::null(),
&host_abi as *const HostApi,
instance,
)
};
assert_eq!(
call_result.code,
AbiErrorCode::Ok as u32,
"add(3, 5) must return Ok"
);
assert_eq!(out, 8_u32, "add(3, 5) must equal 8");
println!("smoke_rust_codegen_dispatch: add(3, 5) = {} ✓", out);
core::mem::forget(library);
}
#[test]
fn smoke_cpp_codegen_dispatch() {
let out_dir: PathBuf = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("smoke_cpp_gen");
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 cpp out_dir");
let gen_output: Output = run_polyplugc_cpp(&bundle_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 guest_dir: PathBuf = out_dir.join("guest");
let expected_guest_files: [&str; 4] =
["types.hpp", "contracts.hpp", "interfaces.hpp", "init.hpp"];
for filename in expected_guest_files {
let file_path: PathBuf = guest_dir.join(filename);
assert!(
file_path.exists(),
"expected generated C++ guest file not found: {}",
file_path.display()
);
}
let manifest_path: PathBuf = out_dir.join("manifest.toml");
assert!(
manifest_path.exists(),
"expected manifest.toml not found: {}",
manifest_path.display()
);
println!(
"smoke_cpp_codegen_dispatch: all 5 C++ 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 host_libs_cpp: PathBuf = workspace_root().join("sdks").join("cpp").join("abi");
let interfaces_hpp: PathBuf = guest_dir.join("interfaces.hpp");
let out_obj: PathBuf =
PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("smoke_cpp_interfaces.o");
let compile_result: std::process::Output = Command::new("g++")
.arg("-std=c++20")
.arg(format!("-I{}", host_libs_cpp.display()))
.arg(format!("-I{}", guest_dir.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!("smoke_cpp_codegen_dispatch: interfaces.hpp compiled successfully ✓");
} else {
eprintln!("skipping g++ compile check: g++ --version returned non-zero");
}
} else {
eprintln!("skipping g++ compile check: g++ not found");
}
}
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()
}