#![allow(clippy::expect_used)]
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::ExitStatus;
use polyplug_abi::AbiError;
use polyplug_abi::AbiErrorCode;
use polyplug_abi::BundleInitContext;
use polyplug_abi::GuestContractInterface;
use polyplug_abi::HostApi;
use polyplug_abi::PluginDescriptor;
use polyplug_abi::StringView;
use polyplug_abi::ffi::polyplug_host_alloc;
use polyplug_abi::ffi::polyplug_host_free;
mod common;
use common::polyplugc_bin;
static mut CAPTURED_INTERFACE_PTR: *const GuestContractInterface = core::ptr::null();
unsafe extern "C" fn capture_register_callback(
_this: *const HostApi,
_descriptor: *const PluginDescriptor,
interface: *const GuestContractInterface,
out_err: *mut AbiError,
) {
unsafe {
CAPTURED_INTERFACE_PTR = interface;
}
if !out_err.is_null() {
unsafe {
out_err.write(AbiError {
code: AbiErrorCode::Ok as u32,
message: StringView::null(),
})
};
}
}
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,
) -> polyplug_abi::GuestContractHandle {
polyplug_abi::GuestContractHandle::null()
}
unsafe extern "C" fn noop_find_all_guest_contracts(
_this: *const HostApi,
_contract_id: u64,
_min_version: u32,
) -> polyplug_abi::Array<polyplug_abi::GuestContractHandle> {
polyplug_abi::Array::empty()
}
unsafe extern "C" fn noop_resolve_guest_contract(
_this: *const HostApi,
_handle: polyplug_abi::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()) };
}
}
#[test]
fn test_panic_returns_abi_error_panic() {
let manifest_dir: &Path = Path::new(env!("CARGO_MANIFEST_DIR"));
let workspace_root: &Path = manifest_dir
.parent()
.expect("crates parent")
.parent()
.expect("workspace root");
let api_toml: PathBuf = workspace_root
.join("tests")
.join("fixtures")
.join("test_panic_api.toml");
let tmp_dir: PathBuf = std::env::temp_dir().join("polyplug_panic_plugin_test");
std::fs::create_dir_all(&tmp_dir).expect("create tmp dir");
std::fs::create_dir_all(tmp_dir.join("src")).expect("create tmp src dir");
let bundle_toml_content: String = format!(
"[bundle]\n\
name = \"panic_plugin\"\n\
version = \"1.0.0\"\n\
loader = \"native\"\n\
api = \"{}\"\n\
\n\
[bundle.file]\n\
linux.x86_64 = \"libpanic_plugin.so\"\n\
\n\
[[plugin]]\n\
name = \"panic_plugin\"\n\
implements = [\"test.panic@1.0\"]\n",
api_toml.to_string_lossy().replace('\\', "/")
);
let bundle_toml_path: PathBuf = tmp_dir.join("bundle.toml");
std::fs::write(&bundle_toml_path, bundle_toml_content).expect("write bundle.toml");
let gen_status: ExitStatus = Command::new(polyplugc_bin())
.arg("generate")
.arg("--bundle")
.arg(&bundle_toml_path)
.arg("--lang")
.arg("rust")
.arg("--out")
.arg(tmp_dir.join("src"))
.status()
.expect("polyplugc generate failed to run");
assert!(
gen_status.success(),
"polyplugc generate exited with non-zero status"
);
let guest_lib_path: PathBuf = workspace_root.join("sdks").join("rust").join("guest");
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 cargo_toml_content: String = format!(
"[package]\n\
name = \"panic_plugin\"\n\
version = \"0.1.0\"\n\
edition = \"2024\"\n\
\n\
[lib]\n\
name = \"panic_plugin\"\n\
crate-type = [\"cdylib\"]\n\
\n\
[dependencies]\n\
polyplug_abi = {{ path = \"{}\" }}\n\
polyplug_guest = {{ path = \"{}\" }}\n\
polyplug_utils = {{ path = \"{}\" }}\n",
abi_lib_path.to_string_lossy().replace('\\', "/"),
guest_lib_path.to_string_lossy().replace('\\', "/"),
utils_lib_path.to_string_lossy().replace('\\', "/")
);
std::fs::write(tmp_dir.join("Cargo.toml"), &cargo_toml_content).expect("write Cargo.toml");
let lib_rs_content: &str = concat!(
"// THIS FILE IS WRITTEN BY THE integration_panic TEST -- NOT generated by polyplugc.\n",
"// It implements the TestPanicPlugin trait with a function that always panics.\n",
"\n",
"mod guest {\n",
" pub mod types;\n",
" pub mod contracts;\n",
" pub mod interfaces;\n",
"}\n",
"\n",
"use polyplug_abi::AbiError;\n",
"use polyplug_abi::HostApi;\n",
"use polyplug_abi::BundleInitContext;\n",
"use polyplug_abi::PluginDescriptor;\n",
"use polyplug_guest::GuestError;\n",
"use polyplug_abi::GuestContractInterface;\n",
"use polyplug_abi::StringView;\n",
"use polyplug_abi::AbiErrorCode;\n",
"use guest::interfaces::PANIC_PLUGIN_INTERFACE;\n",
"use guest::contracts::TestPanicGuestContract;\n",
"use polyplug_guest::HostContext;\n",
"\n",
"struct PanicPlugin;\n",
"\n",
"impl TestPanicGuestContract for PanicPlugin {\n",
" fn do_panic(&self) -> Result<(), GuestError> {\n",
" panic!(\"intentional test panic\");\n",
" }\n",
"}\n",
"\n",
"/// Factory called by the generated create_instance for every instance.\n",
"#[unsafe(no_mangle)]\n",
"pub fn polyplug_create_panic_plugin(_host: HostContext) -> Box<dyn TestPanicGuestContract> {\n",
" Box::new(PanicPlugin)\n",
"}\n",
"\n",
"/// Register the panic plugin interface with the host.\n",
"///\n",
"/// # Safety\n",
"/// `host` and `ctx` must be valid pointers.\n",
"#[unsafe(no_mangle)]\n",
"pub unsafe extern \"C\" fn polyplug_init(\n",
" host: *const HostApi,\n",
" ctx: *const BundleInitContext,\n",
") -> AbiError {\n",
" if host.is_null() || ctx.is_null() {\n",
" return AbiError {\n",
" code: AbiErrorCode::Generic as u32,\n",
" message: StringView::null(),\n",
" };\n",
" }\n",
" // SAFETY: host is non-null and valid per ABI contract.\n",
" let host_iface: &HostApi = unsafe { &*host };\n",
" let desc: PluginDescriptor = PluginDescriptor {\n",
" name: StringView {\n",
" ptr: b\"panic_plugin\".as_ptr(),\n",
" len: 12_usize,\n",
" },\n",
" contract_name: StringView {\n",
" ptr: b\"test.panic\".as_ptr(),\n",
" len: 10_usize,\n",
" },\n",
" version: polyplug_abi::Version { major: 1, minor: 0, patch: 0 },\n",
" };\n",
" // Out-param ABI: register_guest_contract writes its AbiError through a\n",
" // trailing pointer and returns void; init still surfaces it by value.\n",
" let mut err: AbiError = AbiError { code: AbiErrorCode::Ok as u32, message: StringView::null() };\n",
" // SAFETY: desc and interface are valid for the duration of the call; &mut err is valid.\n",
" unsafe {\n",
" (host_iface.register_guest_contract)(\n",
" host,\n",
" &desc as *const PluginDescriptor,\n",
" &PANIC_PLUGIN_INTERFACE as *const GuestContractInterface,\n",
" &mut err as *mut AbiError,\n",
" );\n",
" }\n",
" err\n",
"}\n",
);
std::fs::write(tmp_dir.join("src").join("lib.rs"), lib_rs_content).expect("write src/lib.rs");
let build_status: ExitStatus = Command::new(env!("CARGO"))
.arg("build")
.arg("--manifest-path")
.arg(tmp_dir.join("Cargo.toml"))
.arg("--release")
.status()
.expect("cargo build failed to run");
assert!(
build_status.success(),
"cargo build for panic_plugin cdylib failed"
);
let lib_filename: &str = if cfg!(target_os = "macos") {
"libpanic_plugin.dylib"
} else if cfg!(target_os = "windows") {
"panic_plugin.dll"
} else {
"libpanic_plugin.so"
};
let so_path: PathBuf = tmp_dir.join("target").join("release").join(lib_filename);
let library: libloading::Library = unsafe {
libloading::Library::new(&so_path).expect("failed to load panic_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")
};
let host_interface: HostApi = HostApi {
runtime: core::ptr::null_mut(),
register_guest_contract: capture_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(),
};
let ctx: BundleInitContext = BundleInitContext {
bundle_path: StringView::null(),
bundle_id: 0,
};
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 succeed (code Ok)"
);
let interface_ptr: *const GuestContractInterface = unsafe { CAPTURED_INTERFACE_PTR };
assert!(
!interface_ptr.is_null(),
"interface pointer must be non-null"
);
let interface: &GuestContractInterface = unsafe { &*interface_ptr };
let mut instance: polyplug_abi::GuestContractInstance =
polyplug_abi::GuestContractInstance::null();
unsafe {
(interface.create_instance)(
polyplug_abi::VmLoaderData::null(),
&host_interface as *const HostApi,
core::ptr::null(),
&mut instance,
)
};
assert!(
!instance.data.is_null(),
"create_instance must produce a non-null instance payload"
);
let fn_ptr: *const () = unsafe { *interface.dispatch.native.functions.add(0) };
let dispatch_fn: unsafe extern "C" fn(
polyplug_abi::GuestContractInstance,
*const (),
*mut (),
*mut AbiError,
) =
unsafe { core::mem::transmute(fn_ptr) };
let mut call_result: AbiError = AbiError::ok();
unsafe {
dispatch_fn(
instance,
core::ptr::null(),
core::ptr::null_mut(),
&mut call_result,
)
};
assert_eq!(
call_result.code,
AbiErrorCode::Panic as u32,
"do_panic ABI wrapper must return AbiErrorCode::Panic, got {:?}",
call_result.code
);
core::mem::forget(library);
}
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()
}