use core::str::FromStr;
use core::sync::atomic::AtomicUsize;
use core::sync::atomic::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::RwLock;
use std::thread::ThreadId;
use polyplug_abi::runtime::{Compatibility, RuntimeConfig};
use polyplug_abi::types::LogLevel;
use polyplug_abi::{
AbiError, AbiErrorCode, Array, DependencyInfo, GuestContractHandle, GuestContractInterface,
HostApi, HostContractInstance, HostContractInterface, PluginDescriptor, StringView,
SupportedLanguage, types::Version,
};
use polyplug_utils::{BundleId, GuestContractId};
use crate::error::HostContractError;
use crate::error::LoaderError;
use crate::error::RegistryError;
use crate::error::RuntimeError;
use crate::loader::BundleLoader;
use crate::loader::ManifestData;
use crate::loader::ManifestDependency;
use crate::logger::{LoggerHandle, RecoverPoisoned, RecoveringGuard};
pub use crate::runtime_builder::RuntimeBuilder;
use crate::runtime_store::RuntimeStore;
pub(crate) struct ReloadCallback(
pub(crate) Arc<dyn Fn(*mut core::ffi::c_void, polyplug_abi::runtime::ReloadPhase) + Send + Sync>,
);
pub(crate) struct LoadOptions {
pub compatibility: Compatibility,
pub ignore_function_count_mismatch: bool,
}
pub struct Runtime {
pub(crate) registry: Arc<RuntimeStore>,
pub(crate) loaders: RwLock<HashMap<String, Box<dyn BundleLoader>>>,
pub(crate) bundle_manifests: Mutex<HashMap<String, ManifestData>>,
pub(crate) on_reload_cb: Option<ReloadCallback>,
pub(crate) config: RuntimeConfig,
pub(crate) logger: LoggerHandle,
pub(crate) _logger_closure: Option<Box<crate::logger::LoggerClosure>>,
pub(crate) last_error: Mutex<String>,
pub(crate) host_contracts: RwLock<HashMap<u64, &'static HostContractInterface>>,
pub(crate) singleton_instances: RwLock<HashMap<u64, HostContractInstance>>,
pub(crate) host_language: SupportedLanguage,
pub(crate) init_bundle_stack: Mutex<HashMap<ThreadId, Vec<u64>>>,
pub(crate) active_init_count: AtomicUsize,
pub(crate) reload_serialize: Mutex<()>,
pub(crate) instance_counts: Mutex<HashMap<GuestContractId, u64>>,
pub(crate) host_abi: Box<HostApi>,
}
impl Runtime {
pub fn builder() -> RuntimeBuilder {
RuntimeBuilder::new()
}
#[inline(always)]
pub fn find_guest_contract(
&self,
contract_id: u64,
min_version: u32,
) -> Result<GuestContractHandle, RegistryError> {
self.registry
.find_guest_contract(GuestContractId::from_u64(contract_id), min_version)
}
#[inline(always)]
pub fn find_guest_contract_by_bundle(
&self,
bundle_id: u64,
contract_id: u64,
min_version: u32,
) -> Result<GuestContractHandle, RegistryError> {
self.registry.find_guest_contract_by_bundle(
BundleId::from_u64(bundle_id),
GuestContractId::from_u64(contract_id),
min_version,
)
}
#[inline(always)]
pub fn find_all_by_contract(
&self,
contract_id: u64,
min_version: u32,
out: &mut [GuestContractHandle],
) -> usize {
self.registry.find_all_guest_contracts(
GuestContractId::from_u64(contract_id),
min_version,
out,
)
}
#[inline(always)]
pub fn find_all_by_contract_packed(
&self,
contract_id: u64,
min_version: u32,
out: &mut [u64],
) -> usize {
self.registry.find_all_guest_contracts_packed(
GuestContractId::from_u64(contract_id),
min_version,
out,
)
}
#[inline(always)]
pub fn resolve_guest_contract(
&self,
handle: GuestContractHandle,
) -> Result<*const GuestContractInterface, RegistryError> {
self.registry.resolve_guest_contract(handle)
}
pub fn register_host_contract(
&self,
contract_id: u64,
interface: &'static HostContractInterface,
) -> Result<(), HostContractError> {
let mut guard: RecoveringGuard<
std::sync::RwLockWriteGuard<'_, HashMap<u64, &'static HostContractInterface>>,
> = self
.host_contracts
.write()
.recover_poisoned(self.logger, "runtime");
if guard.contains_key(&contract_id) {
return Err(HostContractError::DuplicateContract { contract_id });
}
guard.insert(contract_id, interface);
Ok(())
}
pub fn unregister_host_contract(&self, contract_id: u64) -> bool {
let mut guard: RecoveringGuard<
std::sync::RwLockWriteGuard<'_, HashMap<u64, &'static HostContractInterface>>,
> = self
.host_contracts
.write()
.recover_poisoned(self.logger, "runtime");
guard.remove(&contract_id).is_some()
}
pub fn get_host_contract(
&self,
contract_id: u64,
min_version: u32,
) -> Option<&'static HostContractInterface> {
let guard: RecoveringGuard<
std::sync::RwLockReadGuard<'_, HashMap<u64, &'static HostContractInterface>>,
> = self
.host_contracts
.read()
.recover_poisoned(self.logger, "runtime");
guard.get(&contract_id).and_then(|interface| {
if host_contract_version_satisfies(interface, min_version) {
Some(*interface)
} else {
None
}
})
}
#[inline(always)]
pub fn host_language(&self) -> SupportedLanguage {
self.host_language
}
#[inline(always)]
pub fn host_abi(&self) -> *const HostApi {
&*self.host_abi as *const HostApi
}
#[inline(always)]
pub fn as_context_ptr(&self) -> *const HostApi {
&*self.host_abi as *const HostApi
}
#[inline(always)]
pub fn registry(&self) -> &Arc<RuntimeStore> {
&self.registry
}
#[inline(always)]
pub fn config(&self) -> &RuntimeConfig {
&self.config
}
#[inline(always)]
pub(crate) fn on_reload_cb(&self) -> &Option<ReloadCallback> {
&self.on_reload_cb
}
pub fn emit_warning(&self, msg: &str) {
self.logger
.log(LogLevel::Warn, "runtime", || msg.to_owned());
}
pub fn logger(&self) -> crate::logger::LoggerHandle {
self.logger
}
pub(crate) fn set_last_error(&self, msg: impl Into<String>) {
let mut guard: RecoveringGuard<std::sync::MutexGuard<'_, String>> = self
.last_error
.lock()
.recover_poisoned(self.logger, "runtime");
**guard = msg.into();
}
fn note_instance_created(&self, contract_id: GuestContractId) {
let mut guard: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<GuestContractId, u64>>> =
self.instance_counts
.lock()
.recover_poisoned(self.logger, "runtime");
let entry: &mut u64 = guard.entry(contract_id).or_insert(0);
*entry += 1;
}
fn note_instance_destroyed(&self, contract_id: GuestContractId) {
let mut guard: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<GuestContractId, u64>>> =
self.instance_counts
.lock()
.recover_poisoned(self.logger, "runtime");
if let Some(entry) = guard.get_mut(&contract_id) {
*entry = entry.saturating_sub(1);
if *entry == 0 {
guard.remove(&contract_id);
}
}
}
pub(crate) fn reset_instance_counts_for_contracts(&self, contracts: &[GuestContractId]) {
let mut guard: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<GuestContractId, u64>>> =
self.instance_counts
.lock()
.recover_poisoned(self.logger, "runtime");
contracts.iter().for_each(|cid: &GuestContractId| {
guard.remove(cid);
});
}
pub(crate) fn live_instance_count_for_contracts(&self, contracts: &[GuestContractId]) -> u64 {
let guard: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<GuestContractId, u64>>> = self
.instance_counts
.lock()
.recover_poisoned(self.logger, "runtime");
contracts
.iter()
.map(|cid: &GuestContractId| guard.get(cid).copied().unwrap_or(0))
.sum()
}
pub(crate) fn get_last_error(&self, buf: &mut [u8]) -> usize {
let guard: RecoveringGuard<std::sync::MutexGuard<'_, String>> = self
.last_error
.lock()
.recover_poisoned(self.logger, "runtime");
let bytes: &[u8] = guard.as_bytes();
let write_n: usize = bytes.len().min(buf.len());
if write_n > 0 {
buf[..write_n].copy_from_slice(&bytes[..write_n]);
}
write_n
}
pub(crate) fn clear_last_error(&self) {
let mut guard: RecoveringGuard<std::sync::MutexGuard<'_, String>> = self
.last_error
.lock()
.recover_poisoned(self.logger, "runtime");
guard.clear();
}
pub(crate) fn last_error_len(&self) -> usize {
let guard: RecoveringGuard<std::sync::MutexGuard<'_, String>> = self
.last_error
.lock()
.recover_poisoned(self.logger, "runtime");
guard.len()
}
pub fn register_loader(&self, loader: Box<dyn BundleLoader>) -> Result<(), RuntimeError> {
let name: String = loader.loader_name().to_string();
let mut loaders: RecoveringGuard<
std::sync::RwLockWriteGuard<'_, HashMap<String, Box<dyn BundleLoader>>>,
> = self
.loaders
.write()
.recover_poisoned(self.logger, "runtime");
if loaders.contains_key(&name) {
return Err(RuntimeError::Loader(LoaderError::DuplicateLoader {
loader_name: name,
}));
}
loaders.insert(name, loader);
Ok(())
}
pub(crate) fn loader_for(&self, loader_name: &str) -> Option<&dyn BundleLoader> {
let loaders: RecoveringGuard<
std::sync::RwLockReadGuard<'_, HashMap<String, Box<dyn BundleLoader>>>,
> = self.loaders.read().recover_poisoned(self.logger, "runtime");
let loader_ptr: *const dyn BundleLoader = loaders.get(loader_name).map(Box::as_ref)?;
Some(unsafe { &*loader_ptr })
}
pub fn push_init_bundle_id(&self, bundle_id: u64) {
let mut stack: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<ThreadId, Vec<u64>>>> =
self.init_bundle_stack
.lock()
.recover_poisoned(self.logger, "runtime");
stack
.entry(std::thread::current().id())
.or_default()
.push(bundle_id);
self.active_init_count.fetch_add(1, Ordering::Relaxed);
}
pub fn pop_init_bundle_id(&self) {
let thread_id: ThreadId = std::thread::current().id();
let mut stack: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<ThreadId, Vec<u64>>>> =
self.init_bundle_stack
.lock()
.recover_poisoned(self.logger, "runtime");
if let Some(thread_stack) = stack.get_mut(&thread_id) {
if thread_stack.pop().is_some() {
self.active_init_count.fetch_sub(1, Ordering::Relaxed);
}
if thread_stack.is_empty() {
stack.remove(&thread_id);
}
}
}
pub(crate) fn current_init_bundle_id(&self) -> u64 {
if self.active_init_count.load(Ordering::Relaxed) == 0 {
return 0;
}
let thread_id: ThreadId = std::thread::current().id();
let stack: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<ThreadId, Vec<u64>>>> = self
.init_bundle_stack
.lock()
.recover_poisoned(self.logger, "runtime");
stack
.get(&thread_id)
.and_then(|thread_stack| thread_stack.last().copied())
.unwrap_or(0)
}
pub fn load_bundle(&self, path: &Path) -> Result<(), RuntimeError> {
let compatibility: Compatibility = self.config.compatibility;
self.load_bundle_with(
path,
LoadOptions {
compatibility,
ignore_function_count_mismatch: false,
},
)
}
pub fn load_bundle_from_source(
&self,
manifest: ManifestData,
source: crate::loader::BundleSource,
) -> Result<(), RuntimeError> {
let compatibility: Compatibility = self.config.compatibility;
self.load_manifest_with_source(
manifest,
source,
LoadOptions {
compatibility,
ignore_function_count_mismatch: false,
},
)
}
pub fn unload_bundle(&self, bundle_id: BundleId) -> Result<(), RuntimeError> {
let descriptor: crate::runtime_store::BundleDescriptor = self
.registry
.get_bundle_descriptor(bundle_id)
.ok_or_else(|| RuntimeError::BundleNotFound {
bundle_name: format!("{:#x}", bundle_id.id()),
contract_name: String::new(),
})?;
let exported: HashSet<GuestContractId> = self
.registry
.bundle_exported_contracts(bundle_id)
.into_iter()
.collect();
let mut dependents: Vec<String> = self
.registry
.bundles_depending_on_any(&exported)
.into_iter()
.filter(|dep: &BundleId| *dep != bundle_id)
.filter_map(|dep: BundleId| {
self.registry
.get_bundle_descriptor(dep)
.map(|d: crate::runtime_store::BundleDescriptor| d.name)
})
.collect();
if !dependents.is_empty() {
dependents.sort();
return Err(RuntimeError::DependencyInUse {
provider: descriptor.name,
dependents,
});
}
let loader_name: Option<String> = self.bundle_loader_name(&descriptor.name);
self.fire_unloading(bundle_id, &descriptor.name);
let live: u64 = self
.live_instance_count_for_contracts(&self.registry.bundle_exported_contracts(bundle_id));
if live > 0 {
let name: String = descriptor.name.clone();
self.logger.log(LogLevel::Warn, "runtime", || {
format!(
"unload: bundle '{name}' still has {live} live guest instance(s) across its \
contracts; destroy them before unload to avoid use-after-free. Proceeding anyway."
)
});
}
let _count: u32 = self.registry.invalidate_bundle(bundle_id)?;
self.reclaim_via_loader(bundle_id, loader_name.as_deref())?;
Ok(())
}
fn fire_unloading(&self, bundle_id: BundleId, bundle_name: &str) {
if let Some(cb) = self.on_reload_cb() {
let name_view: polyplug_abi::types::StringView = polyplug_abi::types::StringView {
ptr: bundle_name.as_ptr(),
len: bundle_name.len(),
};
(cb.0)(
self.config().on_reload_user_data,
polyplug_abi::runtime::ReloadPhase::unloading(bundle_id, name_view),
);
}
}
fn bundle_loader_name(&self, bundle_name: &str) -> Option<String> {
let manifests: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<String, ManifestData>>> =
self.bundle_manifests
.lock()
.recover_poisoned(self.logger, "runtime");
manifests
.get(bundle_name)
.map(|m: &ManifestData| m.loader.clone())
}
fn reclaim_via_loader(
&self,
bundle_id: BundleId,
loader_name: Option<&str>,
) -> Result<(), RuntimeError> {
let name: &str = match loader_name {
Some(n) => n,
None => return Ok(()),
};
match self.loader_for(name) {
Some(loader) => loader.unload(bundle_id, self).map_err(RuntimeError::Loader),
None => Ok(()),
}
}
pub fn unload_bundle_cascade(&self, bundle_id: BundleId) -> Result<(), RuntimeError> {
let mut visited: HashSet<BundleId> = HashSet::new();
self.unload_bundle_cascade_with_visited(bundle_id, &mut visited)
}
fn unload_bundle_cascade_with_visited(
&self,
bundle_id: BundleId,
visited: &mut HashSet<BundleId>,
) -> Result<(), RuntimeError> {
if !visited.insert(bundle_id) {
return Ok(());
}
if self.registry.get_bundle_descriptor(bundle_id).is_none() {
return Err(RuntimeError::BundleNotFound {
bundle_name: format!("{:#x}", bundle_id.id()),
contract_name: String::new(),
});
}
let exported: HashSet<GuestContractId> = self
.registry
.bundle_exported_contracts(bundle_id)
.into_iter()
.collect();
let dependents: Vec<BundleId> = self
.registry
.bundles_depending_on_any(&exported)
.into_iter()
.filter(|dep: &BundleId| *dep != bundle_id)
.collect();
for dep in dependents {
self.unload_bundle_cascade_with_visited(dep, visited)?;
}
let bundle_name: String = self
.registry
.get_bundle_descriptor(bundle_id)
.map(|d: crate::runtime_store::BundleDescriptor| d.name)
.unwrap_or_default();
let loader_name: Option<String> = self.bundle_loader_name(&bundle_name);
self.fire_unloading(bundle_id, &bundle_name);
let live: u64 = self
.live_instance_count_for_contracts(&self.registry.bundle_exported_contracts(bundle_id));
if live > 0 {
let name: String = bundle_name.clone();
self.logger.log(LogLevel::Warn, "runtime", || {
format!(
"unload: bundle '{name}' still has {live} live guest instance(s) across its \
contracts; destroy them before unload to avoid use-after-free. Proceeding anyway."
)
});
}
let _count: u32 = self.registry.invalidate_bundle(bundle_id)?;
self.reclaim_via_loader(bundle_id, loader_name.as_deref())?;
Ok(())
}
pub(crate) fn load_bundle_with(
&self,
path: &Path,
opts: LoadOptions,
) -> Result<(), RuntimeError> {
let bundle_dir: &Path = if path.is_file() {
path.parent().unwrap_or(path)
} else {
path
};
let manifest: ManifestData = crate::loader::parse_manifest(bundle_dir)
.map_err(|e: LoaderError| RuntimeError::Loader(e))?;
let source: crate::loader::BundleSource =
crate::loader::BundleSource::Path(manifest.path.clone());
self.load_manifest_with_source(manifest, source, opts)
}
pub(crate) fn load_manifest_with_source(
&self,
manifest: ManifestData,
source: crate::loader::BundleSource,
opts: LoadOptions,
) -> Result<(), RuntimeError> {
manifest
.validate()
.map_err(|e: LoaderError| RuntimeError::Loader(e))?;
if !opts.ignore_function_count_mismatch {
let major_str: &str = match manifest.version.split_once('.') {
Some((maj, _)) => maj,
None => "0",
};
for contract in &manifest.provides {
let contract_name: &str = match contract.split_once('@') {
Some((name, _)) => name,
None => contract,
};
let key: String = format!("{}@{}", contract_name, major_str);
if !manifest.function_count.contains_key(&key)
&& opts.compatibility != Compatibility::Yolo
{
let msg: String = format!(
"bundle {:?} provides {:?} but has no function_count entry for key {:?}",
manifest.name, contract, key
);
if opts.compatibility == Compatibility::Strict {
return Err(RuntimeError::Loader(LoaderError::FunctionCountMismatch {
contract: contract.clone(),
expected: 0,
found: 0,
}));
} else {
self.emit_warning(&msg);
}
}
}
}
let loader_name: &str = &manifest.loader;
let loader: &dyn BundleLoader = self.loader_for(loader_name).ok_or_else(|| {
RuntimeError::Loader(LoaderError::NoLoaderForName {
bundle: manifest.name.clone(),
loader_name: loader_name.to_owned(),
})
})?;
let declared_contract_ids: Vec<GuestContractId> = manifest
.dependencies
.iter()
.map(|dep: &crate::loader::RawManifestDependency| dep.contract_id)
.collect();
let bundle_id: BundleId = BundleId::new(&manifest.name);
if let Err(e) = self
.registry
.declare_bundle_dependencies(bundle_id, declared_contract_ids)
{
return Err(RuntimeError::Registry(e));
}
let result: Result<(), RuntimeError> = loader
.load(&manifest, &source, self)
.map_err(RuntimeError::Loader);
if result.is_ok() {
let bundle_name: String = manifest.name.clone();
let bundle_deps: Vec<crate::runtime_store::BundleDependency> =
manifest.parsed_bundle_dependencies();
let bundle_version: Version =
parse_manifest_version(&manifest.version, &manifest.name, &manifest.path)?;
if !is_known_runtime_language(&manifest.loader) {
self.logger.log(LogLevel::Warn, "runtime", || {
format!(
"bundle `{}`: unknown loader `{}`; defaulting SupportedLanguage to Rust",
manifest.name, manifest.loader
)
});
}
let runtime_lang: SupportedLanguage = supported_language_from_str(&manifest.loader);
self.registry.register_bundle_metadata(
bundle_id,
manifest.name.clone(),
bundle_version,
runtime_lang,
manifest.path.clone(),
bundle_deps,
)?;
if !opts.ignore_function_count_mismatch && opts.compatibility != Compatibility::Yolo {
self.validate_loaded_function_counts(bundle_id, &manifest, opts.compatibility)?;
}
let mut manifests: RecoveringGuard<
std::sync::MutexGuard<'_, HashMap<String, ManifestData>>,
> = self
.bundle_manifests
.lock()
.recover_poisoned(self.logger, "runtime");
manifests.insert(bundle_name, manifest);
}
result
}
fn validate_loaded_function_counts(
&self,
bundle_id: BundleId,
manifest: &ManifestData,
compatibility: Compatibility,
) -> Result<(), RuntimeError> {
let registered: Vec<(String, u32, Option<u32>)> =
self.registry.bundle_native_function_counts(bundle_id);
for (contract_name, major, actual_opt) in registered {
let actual: u32 = match actual_opt {
Some(n) => n,
None => continue, };
let key: String = format!("{}@{}", contract_name, major);
let declared: u32 = match manifest.function_count.get(&key) {
Some(n) => *n,
None => continue, };
if declared != actual {
match compatibility {
Compatibility::Strict => {
return Err(RuntimeError::Loader(LoaderError::FunctionCountMismatch {
contract: key,
expected: declared,
found: actual,
}));
}
Compatibility::Relaxed => {
self.logger.log(LogLevel::Warn, "runtime", || {
format!(
"bundle `{}` contract `{}`: declared function_count {} but interface exports {}",
manifest.name, key, declared, actual
)
});
}
Compatibility::Yolo => {}
}
}
}
Ok(())
}
}
pub(crate) fn validate_bundle_compatibility(
manifests: &[(PathBuf, ManifestData)],
compatibility: Compatibility,
logger: LoggerHandle,
) -> Result<(), RuntimeError> {
let mut provider_map: HashMap<String, &ManifestData> = HashMap::new();
for (_path, manifest) in manifests {
for contract in &manifest.provides {
let bare_contract: &str = match contract.split_once('@') {
Some((name, _)) => name,
None => contract.as_str(),
};
provider_map.insert(bare_contract.to_owned(), manifest);
}
}
for (path, manifest) in manifests {
let resolved: Vec<ManifestDependency> = manifest.resolved_dependencies_with_logger(logger);
for dep in &resolved {
let (dep_contract, dep_min_version_str): (&str, &str) = match dep {
ManifestDependency::ByContract {
contract,
min_version,
..
} => (contract.as_str(), min_version.as_str()),
ManifestDependency::ByBundle {
contract,
min_version,
..
} => (contract.as_str(), min_version.as_str()),
};
if dep_min_version_str.is_empty() {
continue;
}
let provider: &ManifestData = match provider_map.get(dep_contract) {
Some(p) => p,
None => continue, };
let required: Version = match Version::from_str(dep_min_version_str) {
Ok(v) => v,
Err(e) => {
return Err(RuntimeError::Loader(LoaderError::ManifestParse {
path: path.display().to_string(),
reason: format!("invalid version '{}': {:?}", dep_min_version_str, e),
}));
}
};
let provided: Version =
parse_manifest_version(&provider.version, &provider.name, path)?;
if !provided.is_compatible_with(&required) {
match compatibility {
Compatibility::Strict => {
return Err(RuntimeError::Loader(LoaderError::VersionMismatch {
contract: dep_contract.to_owned(),
required,
found: provided,
}));
}
Compatibility::Relaxed => {
logger.log(LogLevel::Warn, "runtime", || {
format!(
"version mismatch for contract `{}`: required={}, found={} (bundle `{}`)",
dep_contract, required, provided, provider.name
)
});
}
Compatibility::Yolo => {} }
}
}
for contract in &manifest.provides {
let bare_contract: &str = match contract.split_once('@') {
Some((name, _)) => name,
None => contract.as_str(),
};
let major_str: &str = match manifest.version.split_once('.') {
Some((maj, _)) => maj,
None => "0",
};
let key: String = format!("{}@{}", bare_contract, major_str);
if !manifest.function_count.contains_key(&key) {
match compatibility {
Compatibility::Strict => {
return Err(RuntimeError::Loader(LoaderError::FunctionCountMismatch {
contract: contract.clone(),
expected: 0,
found: 0,
}));
}
Compatibility::Relaxed => {
logger.log(LogLevel::Warn, "runtime", || {
format!(
"bundle `{}` provides `{}` but has no function_count entry for key `{}`",
manifest.name, contract, key
)
});
}
Compatibility::Yolo => {} }
}
}
}
Ok(())
}
fn parse_manifest_version(
v: &str,
_bundle_name: &str,
manifest_path: &std::path::Path,
) -> Result<Version, RuntimeError> {
if v.is_empty() {
return Ok(Version {
major: 0,
minor: 0,
patch: 0,
});
}
match Version::from_str(v) {
Ok(version) => Ok(version),
Err(e) => Err(RuntimeError::Loader(LoaderError::ManifestParse {
path: manifest_path.display().to_string(),
reason: format!("invalid version '{}': {:?}", v, e),
})),
}
}
fn plugin_handle_null() -> GuestContractHandle {
GuestContractHandle::null()
}
fn host_contract_version_satisfies(interface: &HostContractInterface, min_version: u32) -> bool {
if min_version == 0 {
return true;
}
let req_major: u32 = min_version >> 16;
let req_minor: u32 = min_version & 0xFFFF;
interface.contract_version.major == req_major && interface.contract_version.minor >= req_minor
}
fn supported_language_from_str(s: &str) -> SupportedLanguage {
match s {
"native" | "rust" => SupportedLanguage::Rust,
"python" => SupportedLanguage::Python,
"lua" => SupportedLanguage::Lua,
"javascript" | "js" => SupportedLanguage::JavaScript,
"dotnet" | "csharp" => SupportedLanguage::Dotnet,
"cpp" => SupportedLanguage::Cpp,
_ => SupportedLanguage::Rust,
}
}
fn is_known_runtime_language(s: &str) -> bool {
matches!(
s,
"native" | "rust" | "python" | "lua" | "javascript" | "js" | "dotnet" | "csharp" | "cpp"
)
}
unsafe fn string_view_to_string_owned(
sv: &polyplug_abi::types::StringView,
context: &str,
) -> Result<String, RuntimeError> {
if sv.ptr.is_null() || sv.len == 0 {
return Ok(String::new());
}
let slice: &[u8] = unsafe { core::slice::from_raw_parts(sv.ptr, sv.len) };
match core::str::from_utf8(slice) {
Ok(s) => Ok(s.to_owned()),
Err(_) => Err(RuntimeError::InvalidUtf8 {
context: context.to_owned(),
}),
}
}
unsafe fn validate_interface_fn_ptrs(
base: *const u8,
create_offset: usize,
destroy_offset: usize,
dispatch_type_offset: usize,
dispatch_offset: usize,
context: ValidationContext,
) -> Option<&'static str> {
unsafe {
let create_ptr: *const core::ffi::c_void = base
.add(create_offset)
.cast::<*const core::ffi::c_void>()
.read();
if create_ptr.is_null() {
return Some(match context {
ValidationContext::Guest => {
"register_guest_contract: create_instance is null — the field is required; signal create failure by returning a null instance handle instead"
}
ValidationContext::Host => {
"register_host_contract: create_instance is null — the field is required; signal create failure by returning a null instance handle instead"
}
});
}
let destroy_ptr: *const core::ffi::c_void = base
.add(destroy_offset)
.cast::<*const core::ffi::c_void>()
.read();
if destroy_ptr.is_null() {
return Some(match context {
ValidationContext::Guest => {
"register_guest_contract: destroy_instance is null — the field is required; use a no-op function for stateless contracts"
}
ValidationContext::Host => {
"register_host_contract: destroy_instance is null — the field is required; use a no-op function for singleton/stateless contracts"
}
});
}
let dispatch_type_raw: u32 = base.add(dispatch_type_offset).cast::<u32>().read();
if dispatch_type_raw == polyplug_abi::dispatch::DispatchType::VirtualMachine as u32 {
let call_ptr: *const core::ffi::c_void = base
.add(dispatch_offset)
.cast::<*const core::ffi::c_void>()
.read();
if call_ptr.is_null() {
return Some(match context {
ValidationContext::Guest => {
"register_guest_contract: dispatch.vm.call is null — required for VirtualMachine dispatch"
}
ValidationContext::Host => {
"register_host_contract: dispatch.vm.call is null — required for VirtualMachine dispatch"
}
});
}
} else if dispatch_type_raw == polyplug_abi::dispatch::DispatchType::Native as u32 {
let function_count: u32 = base.add(dispatch_offset).cast::<u32>().read();
let functions: *const *const core::ffi::c_void = base
.add(dispatch_offset + 8)
.cast::<*const *const core::ffi::c_void>()
.read();
if function_count > 0 {
if functions.is_null() {
return Some(match context {
ValidationContext::Guest => {
"register_guest_contract: dispatch.native.functions is null while function_count > 0"
}
ValidationContext::Host => {
"register_host_contract: dispatch.native.functions is null while function_count > 0"
}
});
}
for fn_index in 0..function_count as usize {
if functions.add(fn_index).read().is_null() {
return Some(match context {
ValidationContext::Guest => {
"register_guest_contract: dispatch.native.functions contains a null entry within function_count"
}
ValidationContext::Host => {
"register_host_contract: dispatch.native.functions contains a null entry within function_count"
}
});
}
}
}
}
}
None
}
#[derive(Clone, Copy)]
enum ValidationContext {
Guest,
Host,
}
pub(crate) unsafe extern "C" fn host_register_guest_contract(
this: *const HostApi,
descriptor: *const PluginDescriptor,
interface: *const GuestContractInterface,
out_err: *mut polyplug_abi::types::AbiError,
) {
let result: polyplug_abi::types::AbiError =
unsafe { host_register_guest_contract_impl(this, descriptor, interface) };
if !out_err.is_null() {
unsafe { out_err.write(result) };
}
}
unsafe fn host_register_guest_contract_impl(
this: *const HostApi,
descriptor: *const PluginDescriptor,
interface: *const GuestContractInterface,
) -> polyplug_abi::types::AbiError {
if this.is_null() {
return polyplug_abi::types::AbiError {
code: polyplug_abi::types::AbiErrorCode::Generic as u32,
message: polyplug_abi::types::StringView::null(),
};
}
if descriptor.is_null() {
return polyplug_abi::types::AbiError {
code: polyplug_abi::types::AbiErrorCode::InvalidPointer as u32,
message: polyplug_abi::types::StringView::from_static(
b"register_guest_contract: descriptor pointer is null",
),
};
}
if interface.is_null() {
return polyplug_abi::types::AbiError {
code: polyplug_abi::types::AbiErrorCode::InvalidPointer as u32,
message: polyplug_abi::types::StringView::from_static(
b"register_guest_contract: interface pointer is null",
),
};
}
if let Some(violation) = unsafe {
validate_interface_fn_ptrs(
interface.cast::<u8>(),
core::mem::offset_of!(GuestContractInterface, create_instance),
core::mem::offset_of!(GuestContractInterface, destroy_instance),
core::mem::offset_of!(GuestContractInterface, dispatch_type),
core::mem::offset_of!(GuestContractInterface, dispatch),
ValidationContext::Guest,
)
} {
return polyplug_abi::types::AbiError {
code: polyplug_abi::types::AbiErrorCode::InvalidPointer as u32,
message: polyplug_abi::types::StringView::from_static(violation.as_bytes()),
};
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let registry: &RuntimeStore = &runtime.registry;
let bundle_id: u64 = runtime.current_init_bundle_id();
let desc: PluginDescriptor = unsafe { *descriptor };
if desc.contract_name.ptr.is_null() || desc.contract_name.len == 0 {
return polyplug_abi::types::AbiError {
code: polyplug_abi::types::AbiErrorCode::Generic as u32,
message: polyplug_abi::types::StringView::from_static(
b"PluginDescriptor.contract_name is null or empty",
),
};
}
let contract_name: String = match unsafe {
string_view_to_string_owned(&desc.contract_name, "PluginDescriptor.contract_name")
} {
Ok(name) => name,
Err(e) => {
runtime.set_last_error(e.to_string());
runtime.logger.log(LogLevel::Error, "registry", || {
format!("registration rejected for bundle {bundle_id}: {e}")
});
return polyplug_abi::types::AbiError {
code: polyplug_abi::types::AbiErrorCode::Generic as u32,
message: polyplug_abi::types::StringView::null(),
};
}
};
match unsafe {
registry.register_guest_contract(
desc,
interface,
contract_name,
BundleId::from_u64(bundle_id),
)
} {
Ok(_handle) => polyplug_abi::types::AbiError::ok(),
Err(e) => {
runtime.logger.log(LogLevel::Error, "registry", || {
format!("registration failed for bundle {bundle_id}: {e}")
});
runtime.set_last_error(e.to_string());
let code: polyplug_abi::types::AbiErrorCode = match e {
crate::error::RegistryError::DuplicateProvider { .. } => {
polyplug_abi::types::AbiErrorCode::DuplicateProvider
}
_ => polyplug_abi::types::AbiErrorCode::Generic,
};
polyplug_abi::types::AbiError {
code: code as u32,
message: polyplug_abi::types::StringView::null(),
}
}
}
}
pub(crate) unsafe extern "C" fn host_alloc(
_this: *const HostApi,
size: usize,
align: usize,
) -> *mut u8 {
polyplug_abi::ffi::polyplug_host_alloc(size, align)
}
pub(crate) unsafe extern "C" fn host_free(
_this: *const HostApi,
ptr: *mut u8,
size: usize,
align: usize,
) {
unsafe { polyplug_abi::ffi::polyplug_host_free(ptr, size, align) }
}
pub(crate) unsafe extern "C" fn host_find_guest_contract(
this: *const HostApi,
contract_id: u64,
min_version: u32,
) -> GuestContractHandle {
if this.is_null() {
return plugin_handle_null();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let registry: &RuntimeStore = &runtime.registry;
let caller_bundle_id: u64 = runtime.current_init_bundle_id();
if caller_bundle_id != 0
&& !registry.is_bundle_dependency_declared(
BundleId::from_u64(caller_bundle_id),
GuestContractId::from_u64(contract_id),
)
{
return plugin_handle_null();
}
match registry.find_guest_contract(GuestContractId::from_u64(contract_id), min_version) {
Ok(h) => h,
Err(_) => plugin_handle_null(),
}
}
pub(crate) unsafe extern "C" fn host_find_all_guest_contracts(
this: *const HostApi,
contract_id: u64,
min_version: u32,
) -> polyplug_abi::Array<GuestContractHandle> {
if this.is_null() {
return Array::empty();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let registry: &RuntimeStore = &runtime.registry;
let caller_bundle_id: u64 = runtime.current_init_bundle_id();
if caller_bundle_id != 0
&& !registry.is_bundle_dependency_declared(
BundleId::from_u64(caller_bundle_id),
GuestContractId::from_u64(contract_id),
)
{
return Array::empty();
}
let handles: Vec<GuestContractHandle> =
registry.collect_guest_contracts(GuestContractId::from_u64(contract_id), min_version);
if handles.is_empty() {
return Array::empty();
}
let count: usize = handles.len();
let size: usize = count * core::mem::size_of::<GuestContractHandle>();
let align: usize = core::mem::align_of::<GuestContractHandle>();
let ptr: *mut GuestContractHandle =
unsafe { host_alloc(this, size, align) as *mut GuestContractHandle };
if ptr.is_null() {
return Array::empty();
}
unsafe {
core::ptr::copy_nonoverlapping(handles.as_ptr(), ptr, count);
}
Array::new(ptr, count)
}
pub unsafe extern "C" fn host_resolve_guest_contract(
this: *const HostApi,
handle: GuestContractHandle,
) -> *const GuestContractInterface {
if this.is_null() {
return core::ptr::null();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let registry: &RuntimeStore = &runtime.registry;
match registry.resolve_guest_contract(handle) {
Ok(ptr) => ptr,
Err(_) => core::ptr::null(),
}
}
pub(crate) unsafe extern "C" fn host_revision_counter(this: *const HostApi) -> *const u64 {
if this.is_null() {
return core::ptr::null();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
runtime.registry.revision_ptr()
}
pub(crate) unsafe extern "C" fn host_get_host_contract(
this: *const HostApi,
contract_id: u64,
min_version: u32,
) -> HostContractInstance {
if this.is_null() {
return HostContractInstance::null();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let host_contracts_guard: RecoveringGuard<
std::sync::RwLockReadGuard<'_, HashMap<u64, &'static HostContractInterface>>,
> = runtime
.host_contracts
.read()
.recover_poisoned(runtime.logger, "runtime");
let interface: Option<&HostContractInterface> = host_contracts_guard
.values()
.find(|iface| {
iface.contract_id.id() == contract_id
&& host_contract_version_satisfies(iface, min_version)
})
.copied();
match interface {
Some(interface) => {
drop(host_contracts_guard);
if interface.singleton {
let singleton_guard: RecoveringGuard<
std::sync::RwLockReadGuard<'_, HashMap<u64, HostContractInstance>>,
> = runtime
.singleton_instances
.read()
.recover_poisoned(runtime.logger, "runtime");
if let Some(&instance) = singleton_guard.get(&contract_id) {
return instance;
}
drop(singleton_guard);
let mut singleton_guard: RecoveringGuard<
std::sync::RwLockWriteGuard<'_, HashMap<u64, HostContractInstance>>,
> = runtime
.singleton_instances
.write()
.recover_poisoned(runtime.logger, "runtime");
if let Some(&instance) = singleton_guard.get(&contract_id) {
return instance;
}
let mut instance: HostContractInstance = HostContractInstance::null();
unsafe {
(interface.create_instance)(
interface as *const HostContractInterface,
core::ptr::null(),
&mut instance,
)
};
if !instance.is_null() {
singleton_guard.insert(contract_id, instance);
}
instance
} else {
let mut instance: HostContractInstance = HostContractInstance::null();
unsafe {
(interface.create_instance)(
interface as *const HostContractInterface,
core::ptr::null(),
&mut instance,
)
};
instance
}
}
None => {
runtime.set_last_error(format!(
"host contract not found: id={}, min_version={}",
contract_id, min_version
));
HostContractInstance::null()
}
}
}
pub(crate) unsafe extern "C" fn host_resolve_host_contract_interface(
this: *const HostApi,
contract_id: u64,
min_version: u32,
) -> *const HostContractInterface {
if this.is_null() {
return core::ptr::null();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let host_contracts_guard: RecoveringGuard<
std::sync::RwLockReadGuard<'_, HashMap<u64, &'static HostContractInterface>>,
> = runtime
.host_contracts
.read()
.recover_poisoned(runtime.logger, "runtime");
host_contracts_guard
.values()
.find(|iface| {
iface.contract_id.id() == contract_id
&& host_contract_version_satisfies(iface, min_version)
})
.map(|v| *v as *const HostContractInterface)
.unwrap_or_else(|| {
runtime.set_last_error(format!(
"host contract interface not found: id={}, min_version={}",
contract_id, min_version
));
core::ptr::null()
})
}
pub(crate) unsafe extern "C" fn host_list_bundles(
this: *const HostApi,
) -> polyplug_abi::Array<polyplug_utils::BundleId> {
if this.is_null() {
return Array::empty();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let manifests: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<String, ManifestData>>> =
runtime
.bundle_manifests
.lock()
.recover_poisoned(runtime.logger, "runtime");
let count = manifests.len();
if count == 0 {
return Array::empty();
}
let size = count * core::mem::size_of::<BundleId>();
let align = core::mem::align_of::<BundleId>();
let ptr = unsafe { host_alloc(this, size, align) as *mut BundleId };
if ptr.is_null() {
return Array::empty();
}
for (i, (_, manifest)) in manifests.iter().enumerate() {
unsafe {
*ptr.add(i) = BundleId::from_u64(manifest.id);
}
}
Array::new(ptr, count)
}
pub(crate) unsafe extern "C" fn host_get_dependencies(
this: *const HostApi,
) -> polyplug_abi::Array<polyplug_abi::DependencyInfo> {
if this.is_null() {
return Array::empty();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let caller_bundle_id: u64 = runtime.current_init_bundle_id();
if caller_bundle_id == 0 {
return Array::empty();
}
let manifests: RecoveringGuard<std::sync::MutexGuard<'_, HashMap<String, ManifestData>>> =
runtime
.bundle_manifests
.lock()
.recover_poisoned(runtime.logger, "runtime");
let manifest = match manifests.values().find(|m| m.id == caller_bundle_id) {
Some(m) => m,
None => return Array::empty(),
};
let deps = &manifest.dependencies;
if deps.is_empty() {
return Array::empty();
}
let count = deps.len();
let size = count * core::mem::size_of::<DependencyInfo>();
let align = core::mem::align_of::<DependencyInfo>();
let ptr = unsafe { host_alloc(this, size, align) as *mut DependencyInfo };
if ptr.is_null() {
return Array::empty();
}
for (i, dep) in deps.iter().enumerate() {
let info = DependencyInfo {
contract_id: dep.contract_id,
min_version: dep.min_version.parse().unwrap_or(0),
bundle_id: dep
.bundle_id
.unwrap_or_else(|| polyplug_utils::BundleId::from_u64(0)),
};
unsafe {
*ptr.add(i) = info;
}
}
Array::new(ptr, count)
}
pub unsafe extern "C" fn host_load_bundle(
this: *const HostApi,
path: *const u8,
path_len: usize,
out_err: *mut polyplug_abi::AbiError,
) {
let result: polyplug_abi::AbiError = unsafe { host_load_bundle_impl(this, path, path_len) };
if !out_err.is_null() {
unsafe { out_err.write(result) };
}
}
unsafe fn host_load_bundle_impl(
this: *const HostApi,
path: *const u8,
path_len: usize,
) -> polyplug_abi::AbiError {
if this.is_null() {
return AbiError {
code: AbiErrorCode::InvalidPointer as u32,
message: StringView::from_static(b"null HostApi in load_bundle"),
};
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
if path.is_null() {
runtime.set_last_error("null path pointer in load_bundle");
return AbiError {
code: AbiErrorCode::InvalidPointer as u32,
message: StringView::from_static(b"null path pointer in load_bundle"),
};
}
let bytes: &[u8] = unsafe { core::slice::from_raw_parts(path, path_len) };
let s: &str = match core::str::from_utf8(bytes) {
Ok(s) => s,
Err(e) => {
runtime.set_last_error(e.to_string());
return AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
};
}
};
match runtime.load_bundle(std::path::Path::new(s)) {
Ok(()) => AbiError::ok(),
Err(e) => {
runtime.set_last_error(e.to_string());
AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
}
}
}
}
pub unsafe extern "C" fn host_reload_bundle(
this: *const HostApi,
path: *const u8,
path_len: usize,
out_err: *mut polyplug_abi::AbiError,
) {
let result: polyplug_abi::AbiError = unsafe { host_reload_bundle_impl(this, path, path_len) };
if !out_err.is_null() {
unsafe { out_err.write(result) };
}
}
unsafe fn host_reload_bundle_impl(
this: *const HostApi,
path: *const u8,
path_len: usize,
) -> polyplug_abi::AbiError {
if this.is_null() {
return AbiError {
code: AbiErrorCode::InvalidPointer as u32,
message: StringView::from_static(b"null HostApi in reload_bundle"),
};
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
if path.is_null() {
runtime.set_last_error("null path pointer in reload_bundle");
return AbiError {
code: AbiErrorCode::InvalidPointer as u32,
message: StringView::from_static(b"null path pointer in reload_bundle"),
};
}
let bytes: &[u8] = unsafe { core::slice::from_raw_parts(path, path_len) };
let s: &str = match core::str::from_utf8(bytes) {
Ok(s) => s,
Err(e) => {
runtime.set_last_error(e.to_string());
return AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
};
}
};
match runtime.reload_bundle(std::path::Path::new(s)) {
Ok(()) => AbiError::ok(),
Err(e) => {
runtime.set_last_error(e.to_string());
AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
}
}
}
}
pub unsafe extern "C" fn host_unload_bundle(
this: *const HostApi,
bundle_id: BundleId,
out_err: *mut polyplug_abi::AbiError,
) {
let result: polyplug_abi::AbiError = unsafe { host_unload_bundle_impl(this, bundle_id) };
if !out_err.is_null() {
unsafe { out_err.write(result) };
}
}
unsafe fn host_unload_bundle_impl(
this: *const HostApi,
bundle_id: BundleId,
) -> polyplug_abi::AbiError {
if this.is_null() {
return AbiError {
code: AbiErrorCode::InvalidPointer as u32,
message: StringView::from_static(b"null HostApi in unload_bundle"),
};
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
match runtime.unload_bundle(bundle_id) {
Ok(()) => AbiError::ok(),
Err(e) => {
runtime.set_last_error(e.to_string());
AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
}
}
}
}
pub(crate) unsafe extern "C" fn host_register_host_contract(
this: *const HostApi,
interface: *const polyplug_abi::HostContractInterface,
out_err: *mut polyplug_abi::AbiError,
) {
let result: polyplug_abi::AbiError =
unsafe { host_register_host_contract_impl(this, interface) };
if !out_err.is_null() {
unsafe { out_err.write(result) };
}
}
unsafe fn host_register_host_contract_impl(
this: *const HostApi,
interface: *const polyplug_abi::HostContractInterface,
) -> polyplug_abi::AbiError {
if this.is_null() || interface.is_null() {
return AbiError {
code: AbiErrorCode::InvalidPointer as u32,
message: StringView::from_static(b"null pointer in register_host_contract"),
};
}
if let Some(violation) = unsafe {
validate_interface_fn_ptrs(
interface.cast::<u8>(),
core::mem::offset_of!(polyplug_abi::HostContractInterface, create_instance),
core::mem::offset_of!(polyplug_abi::HostContractInterface, destroy_instance),
core::mem::offset_of!(polyplug_abi::HostContractInterface, dispatch_type),
core::mem::offset_of!(polyplug_abi::HostContractInterface, dispatch),
ValidationContext::Host,
)
} {
return AbiError {
code: AbiErrorCode::InvalidPointer as u32,
message: StringView::from_static(violation.as_bytes()),
};
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let interface_ref: &'static polyplug_abi::HostContractInterface = unsafe { &*interface };
match runtime.register_host_contract(interface_ref.contract_id.id(), interface_ref) {
Ok(()) => AbiError::ok(),
Err(crate::error::HostContractError::DuplicateContract { .. }) => AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::from_static(b"duplicate host contract registration"),
},
Err(e) => {
runtime.set_last_error(e.to_string());
AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
}
}
}
}
pub(crate) unsafe extern "C" fn host_register_loader(
this: *const HostApi,
loader_ptr: *mut core::ffi::c_void,
out_err: *mut polyplug_abi::AbiError,
) {
let result: polyplug_abi::AbiError = unsafe { host_register_loader_impl(this, loader_ptr) };
if !out_err.is_null() {
unsafe { out_err.write(result) };
}
}
unsafe fn host_register_loader_impl(
this: *const HostApi,
loader_ptr: *mut core::ffi::c_void,
) -> polyplug_abi::AbiError {
if this.is_null() || loader_ptr.is_null() {
return AbiError {
code: AbiErrorCode::InvalidPointer as u32,
message: StringView::from_static(b"null pointer in register_loader"),
};
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let loader: Box<dyn BundleLoader> =
unsafe { *Box::from_raw(loader_ptr as *mut Box<dyn BundleLoader>) };
match runtime.register_loader(loader) {
Ok(()) => AbiError::ok(),
Err(e) => {
runtime.set_last_error(e.to_string());
AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
}
}
}
}
pub unsafe extern "C" fn host_get_last_error(
this: *const HostApi,
buf: *mut u8,
buf_len: usize,
) -> usize {
if this.is_null() {
return 0;
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
if buf.is_null() {
let len = runtime.last_error_len();
runtime.clear_last_error();
return len;
}
if buf_len == 0 {
runtime.clear_last_error();
return 0;
}
let buf_slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
let len = runtime.get_last_error(buf_slice);
runtime.clear_last_error();
len
}
pub unsafe extern "C" fn host_get_error_len(this: *const HostApi) -> usize {
if this.is_null() {
return b"null HostApi pointer".len();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
runtime.last_error_len()
}
pub(crate) unsafe extern "C" fn host_log(
this: *const HostApi,
level: u32,
scope: StringView,
message: StringView,
) {
if this.is_null() {
return;
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let level: LogLevel = match LogLevel::from_u32(level) {
Some(l) => l,
None => LogLevel::Error,
};
let (scope_str, message_str): (&str, &str) = unsafe { (scope.as_str(), message.as_str()) };
runtime
.logger()
.log(level, scope_str, || message_str.to_owned());
}
pub(crate) unsafe extern "C" fn host_create_guest_instance(
this: *const HostApi,
interface: *const GuestContractInterface,
args: *const core::ffi::c_void,
out_instance: *mut polyplug_abi::guest::GuestContractInstance,
) {
let result: polyplug_abi::guest::GuestContractInstance =
unsafe { host_create_guest_instance_impl(this, interface, args) };
if !out_instance.is_null() {
unsafe { out_instance.write(result) };
}
}
unsafe fn guest_instance_loader_data(
interface: *const GuestContractInterface,
) -> polyplug_abi::dispatch::VmLoaderData {
let dispatch_type: polyplug_abi::dispatch::DispatchType = unsafe { (*interface).dispatch_type };
if dispatch_type == polyplug_abi::dispatch::DispatchType::VirtualMachine {
unsafe { (*interface).dispatch.vm.loader_data }
} else {
polyplug_abi::dispatch::VmLoaderData::null()
}
}
unsafe fn host_create_guest_instance_impl(
this: *const HostApi,
interface: *const GuestContractInterface,
args: *const core::ffi::c_void,
) -> polyplug_abi::guest::GuestContractInstance {
if this.is_null() || interface.is_null() {
return polyplug_abi::guest::GuestContractInstance::null();
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let _g: crossbeam_epoch::Guard = crossbeam_epoch::pin();
let contract_id: GuestContractId = unsafe { (*interface).contract_id };
let loader_data: polyplug_abi::dispatch::VmLoaderData =
unsafe { guest_instance_loader_data(interface) };
let mut inst: polyplug_abi::guest::GuestContractInstance =
polyplug_abi::guest::GuestContractInstance::null();
unsafe { ((*interface).create_instance)(loader_data, this, args.cast::<()>(), &mut inst) };
if !inst.data.is_null() {
runtime.note_instance_created(contract_id);
}
inst
}
pub(crate) unsafe extern "C" fn host_destroy_guest_instance(
this: *const HostApi,
interface: *const GuestContractInterface,
instance: polyplug_abi::guest::GuestContractInstance,
) {
if this.is_null() || interface.is_null() {
return;
}
let runtime: &Runtime = unsafe { &*((*this).runtime as *const Runtime) };
let contract_id: GuestContractId = instance.contract_id;
let _g: crossbeam_epoch::Guard = crossbeam_epoch::pin();
let loader_data: polyplug_abi::dispatch::VmLoaderData =
unsafe { guest_instance_loader_data(interface) };
unsafe { ((*interface).destroy_instance)(loader_data, this, instance) };
if !instance.data.is_null() {
runtime.note_instance_destroyed(contract_id);
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
unsafe extern "C" fn stub_host_log(
_this: *const HostApi,
_level: u32,
_scope: polyplug_abi::StringView,
_message: polyplug_abi::StringView,
) {
}
unsafe extern "C" fn test_create_instance(
_this: *const HostContractInterface,
_args: *const (),
out_instance: *mut HostContractInstance,
) {
if !out_instance.is_null() {
unsafe { out_instance.write(HostContractInstance::null()) };
}
}
unsafe extern "C" fn test_destroy_instance(
_this: *const HostContractInterface,
_instance: HostContractInstance,
) {
}
fn host_contract_interface_with_version(major: u32, minor: u32) -> HostContractInterface {
HostContractInterface {
contract_id: polyplug_utils::HostContractId::from(0xABCD_u64),
contract_version: Version {
major,
minor,
patch: 0,
},
singleton: true,
dispatch_type: polyplug_abi::dispatch::dispatch_type::DispatchType::Native,
runtime: core::ptr::null_mut(),
user_data: core::ptr::null_mut(),
create_instance: test_create_instance,
destroy_instance: test_destroy_instance,
dispatch: polyplug_abi::DispatchMechanisms {
native: polyplug_abi::NativeDispatch {
function_count: 0,
functions: core::ptr::null(),
},
},
}
}
fn pack_min_version(major: u32, minor: u32) -> u32 {
(major << 16) | minor
}
#[test]
fn host_contract_version_exact_major_equal_minor_passes() {
let iface: HostContractInterface = host_contract_interface_with_version(1, 5);
assert!(host_contract_version_satisfies(
&iface,
pack_min_version(1, 5)
));
}
#[test]
fn host_contract_version_higher_minor_passes() {
let iface: HostContractInterface = host_contract_interface_with_version(1, 7);
assert!(host_contract_version_satisfies(
&iface,
pack_min_version(1, 5)
));
}
#[test]
fn host_contract_version_lower_minor_fails() {
let iface: HostContractInterface = host_contract_interface_with_version(1, 4);
assert!(!host_contract_version_satisfies(
&iface,
pack_min_version(1, 5)
));
}
#[test]
fn host_contract_version_higher_major_fails() {
let iface: HostContractInterface = host_contract_interface_with_version(2, 0);
assert!(!host_contract_version_satisfies(
&iface,
pack_min_version(1, 5)
));
}
#[test]
fn host_contract_version_lower_major_fails() {
let iface: HostContractInterface = host_contract_interface_with_version(1, 9);
assert!(!host_contract_version_satisfies(
&iface,
pack_min_version(2, 0)
));
}
#[test]
fn builder_creates_runtime() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let result: Result<GuestContractHandle, _> =
runtime.find_guest_contract(0x1234_5678_9ABC_DEF0_u64, 0);
assert!(result.is_err(), "empty registry should return not found");
}
#[test]
fn abi_ok_constant() {
assert_eq!(
polyplug_abi::AbiErrorCode::Ok,
polyplug_abi::AbiErrorCode::Ok
);
assert_eq!(polyplug_abi::AbiErrorCode::Ok as u32, 0_u32);
}
#[test]
fn host_callbacks_use_host_interface_self_passing() {
assert_eq!(core::mem::size_of::<*const HostApi>(), 8);
}
#[test]
fn host_find_guest_contract_null_this_returns_null() {
let handle: GuestContractHandle =
unsafe { host_find_guest_contract(core::ptr::null(), 0_u64, 0_u32) };
assert!(
handle.is_null(),
"host_find_guest_contract must return null when this is null"
);
}
#[test]
fn dep_enforcement_blocks_undeclared_contract() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
runtime.push_init_bundle_id(0xDEAD_BEEF_u64);
let host_interface: HostApi = HostApi {
runtime: Arc::as_ptr(&runtime) as *mut core::ffi::c_void,
register_guest_contract: host_register_guest_contract,
alloc: host_alloc,
free: host_free,
find_guest_contract: host_find_guest_contract,
find_all_guest_contracts: host_find_all_guest_contracts,
resolve_guest_contract: host_resolve_guest_contract,
get_host_contract: host_get_host_contract,
resolve_host_contract_interface: host_resolve_host_contract_interface,
list_bundles: host_list_bundles,
get_dependencies: host_get_dependencies,
load_bundle: host_load_bundle,
reload_bundle: host_reload_bundle,
register_host_contract: host_register_host_contract,
register_loader: host_register_loader,
get_last_error: host_get_last_error,
get_error_len: host_get_error_len,
unload_bundle: host_unload_bundle,
log: stub_host_log,
create_guest_instance: host_create_guest_instance,
destroy_guest_instance: host_destroy_guest_instance,
revision_counter: host_revision_counter,
reserved: core::ptr::null(),
};
let handle: GuestContractHandle = unsafe {
host_find_guest_contract(
&host_interface as *const HostApi,
0x1111_2222_3333_4444_u64,
0_u32,
)
};
assert!(
handle.is_null(),
"dep enforcement must return null for undeclared contract during init phase"
);
runtime.pop_init_bundle_id();
}
fn create_bundle_dir(temp: &tempfile::TempDir, bundle_name: &str, runtime: &str) -> PathBuf {
let bundle_dir: PathBuf = temp.path().join(bundle_name);
if let Err(e) = std::fs::create_dir_all(&bundle_dir) {
panic!("failed to create bundle dir {}: {e}", bundle_dir.display());
}
let so_path: PathBuf = bundle_dir.join("dummy.so");
if let Err(e) = std::fs::write(&so_path, b"") {
panic!("failed to write dummy so {}: {e}", so_path.display());
}
let manifest: String = format!(
"id = {}\nname = \"{}\"\nloader = \"{}\"\nfile = \"dummy.so\"\n",
BundleId::new(bundle_name).id(),
bundle_name,
runtime
);
let manifest_path: PathBuf = bundle_dir.join("manifest.toml");
if let Err(e) = std::fs::write(&manifest_path, manifest) {
panic!("failed to write manifest {}: {e}", manifest_path.display());
}
bundle_dir
}
fn register_guest_contract(
registry: &crate::runtime_store::RuntimeStore,
contract_id: u64,
bundle_id: u64,
) -> GuestContractHandle {
use polyplug_abi::{
DispatchMechanisms, DispatchType, GuestContractInstance, GuestContractInterface,
NativeDispatch,
};
unsafe extern "C" fn stub_create_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_args: *const (),
out_instance: *mut GuestContractInstance,
) {
if !out_instance.is_null() {
unsafe { out_instance.write(GuestContractInstance::null()) };
}
}
unsafe extern "C" fn stub_destroy_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_instance: GuestContractInstance,
) {
}
let interface: &'static GuestContractInterface =
Box::leak(Box::new(GuestContractInterface {
contract_id: polyplug_utils::GuestContractId::from_u64(contract_id),
contract_version: Version {
major: 0,
minor: 0,
patch: 0,
},
dispatch_type: DispatchType::Native,
create_instance: stub_create_instance,
destroy_instance: stub_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 0,
functions: core::ptr::null(),
},
},
}));
let descriptor: polyplug_abi::PluginDescriptor = polyplug_abi::PluginDescriptor {
name: polyplug_abi::StringView::from_static(b"stub"),
contract_name: polyplug_abi::StringView::from_static(b"stub.contract"),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
};
let result: Result<GuestContractHandle, crate::error::RegistryError> = unsafe {
registry.register_guest_contract(
descriptor,
interface,
"stub.contract".to_owned(),
BundleId::from_u64(bundle_id),
)
};
match result {
Ok(handle) => handle,
Err(e) => panic!("failed to register_guest_contract contract: {e}"),
}
}
unsafe extern "C" fn native_add_one(
_instance: polyplug_abi::guest::GuestContractInstance,
args: *const (),
out: *mut (),
out_err: *mut polyplug_abi::types::AbiError,
) {
unsafe {
let input: i32 = *(args as *const i32);
*(out as *mut i32) = input + 1;
}
if !out_err.is_null() {
unsafe { out_err.write(polyplug_abi::types::AbiError::ok()) };
}
}
struct NativeFnTable([*const (); 1]);
unsafe impl Sync for NativeFnTable {}
static NATIVE_FNS: NativeFnTable = NativeFnTable([native_add_one as *const ()]);
fn register_native_caller_contract(
registry: &crate::runtime_store::RuntimeStore,
contract_id: u64,
bundle_id: u64,
) {
use polyplug_abi::{
DispatchMechanisms, DispatchType, GuestContractInstance, GuestContractInterface,
NativeDispatch,
};
unsafe extern "C" fn stub_create(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_args: *const (),
out_instance: *mut GuestContractInstance,
) {
if !out_instance.is_null() {
unsafe { out_instance.write(GuestContractInstance::null()) };
}
}
unsafe extern "C" fn stub_destroy(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_instance: GuestContractInstance,
) {
}
let interface: &'static GuestContractInterface =
Box::leak(Box::new(GuestContractInterface {
contract_id: GuestContractId::from_u64(contract_id),
contract_version: Version {
major: 1,
minor: 0,
patch: 0,
},
dispatch_type: DispatchType::Native,
create_instance: stub_create,
destroy_instance: stub_destroy,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 1,
functions: NATIVE_FNS.0.as_ptr(),
},
},
}));
let descriptor: polyplug_abi::PluginDescriptor = polyplug_abi::PluginDescriptor {
name: polyplug_abi::StringView::from_static(b"caller"),
contract_name: polyplug_abi::StringView::from_static(b"caller.contract"),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
};
let result: Result<GuestContractHandle, crate::error::RegistryError> = unsafe {
registry.register_guest_contract(
descriptor,
interface,
"caller.contract".to_owned(),
BundleId::from_u64(bundle_id),
)
};
if let Err(e) = result {
panic!("failed to register native caller contract: {e}");
}
}
fn host_with_runtime(runtime: &Arc<Runtime>) -> *const HostApi {
runtime.host_abi()
}
const STATEFUL_CONTRACT_ID: u64 = 0x0BAD_F00D_1234_5678;
unsafe extern "C" fn stateful_create_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
_args: *const (),
out_instance: *mut polyplug_abi::guest::GuestContractInstance,
) {
let boxed: Box<u8> = Box::new(0u8);
let instance: polyplug_abi::guest::GuestContractInstance =
polyplug_abi::guest::GuestContractInstance {
data: Box::into_raw(boxed) as *mut core::ffi::c_void,
contract_id: GuestContractId::from_u64(STATEFUL_CONTRACT_ID),
};
if !out_instance.is_null() {
unsafe { out_instance.write(instance) };
}
}
unsafe extern "C" fn stateful_destroy_instance(
_loader_data: polyplug_abi::dispatch::VmLoaderData,
_host: *const HostApi,
instance: polyplug_abi::guest::GuestContractInstance,
) {
if !instance.data.is_null() {
drop(unsafe { Box::from_raw(instance.data as *mut u8) });
}
}
fn register_stateful_contract(
registry: &crate::runtime_store::RuntimeStore,
contract_id: u64,
bundle_id: u64,
) -> *const GuestContractInterface {
use polyplug_abi::{
DispatchMechanisms, DispatchType, GuestContractInterface, NativeDispatch,
};
let interface: &'static GuestContractInterface =
Box::leak(Box::new(GuestContractInterface {
contract_id: GuestContractId::from_u64(contract_id),
contract_version: Version {
major: 1,
minor: 0,
patch: 0,
},
dispatch_type: DispatchType::Native,
create_instance: stateful_create_instance,
destroy_instance: stateful_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 0,
functions: core::ptr::null(),
},
},
}));
let descriptor: polyplug_abi::PluginDescriptor = polyplug_abi::PluginDescriptor {
name: polyplug_abi::StringView::from_static(b"stateful"),
contract_name: polyplug_abi::StringView::from_static(b"stateful.contract"),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
};
let result: Result<GuestContractHandle, crate::error::RegistryError> = unsafe {
registry.register_guest_contract(
descriptor,
interface,
"stateful.contract".to_owned(),
BundleId::from_u64(bundle_id),
)
};
if let Err(e) = result {
panic!("failed to register stateful contract: {e}");
}
interface as *const GuestContractInterface
}
#[test]
fn host_instance_lifecycle_counts_stateful_instances() {
let runtime: Arc<Runtime> = Runtime::builder().build().expect("build");
let contract_id: u64 = STATEFUL_CONTRACT_ID;
let cid: GuestContractId = GuestContractId::from_u64(contract_id);
let interface: *const GuestContractInterface =
register_stateful_contract(&runtime.registry, contract_id, 0x1);
let host: *const HostApi = host_with_runtime(&runtime);
assert_eq!(
runtime.live_instance_count_for_contracts(&[cid]),
0,
"no instances created yet"
);
let mut inst_a: polyplug_abi::guest::GuestContractInstance =
polyplug_abi::guest::GuestContractInstance::null();
unsafe { host_create_guest_instance(host, interface, core::ptr::null(), &mut inst_a) };
let mut inst_b: polyplug_abi::guest::GuestContractInstance =
polyplug_abi::guest::GuestContractInstance::null();
unsafe { host_create_guest_instance(host, interface, core::ptr::null(), &mut inst_b) };
assert!(!inst_a.data.is_null() && !inst_b.data.is_null());
assert_eq!(
runtime.live_instance_count_for_contracts(&[cid]),
2,
"two stateful instances counted"
);
unsafe { host_destroy_guest_instance(host, interface, inst_a) };
unsafe { host_destroy_guest_instance(host, interface, inst_b) };
assert_eq!(
runtime.live_instance_count_for_contracts(&[cid]),
0,
"count returns to zero after destroy"
);
}
#[test]
fn host_instance_lifecycle_ignores_stateless_instances() {
let runtime: Arc<Runtime> = Runtime::builder().build().expect("build");
let contract_id: u64 = 0x0FED_CBA9_8765_4321;
let cid: GuestContractId = GuestContractId::from_u64(contract_id);
register_native_caller_contract(&runtime.registry, contract_id, 0x1);
let host: *const HostApi = host_with_runtime(&runtime);
let handle: GuestContractHandle = unsafe { host_find_guest_contract(host, contract_id, 0) };
let interface: *const GuestContractInterface =
unsafe { host_resolve_guest_contract(host, handle) };
assert!(!interface.is_null(), "registered contract must resolve");
let mut inst: polyplug_abi::guest::GuestContractInstance =
polyplug_abi::guest::GuestContractInstance::null();
unsafe { host_create_guest_instance(host, interface, core::ptr::null(), &mut inst) };
assert!(inst.data.is_null(), "stateless instance has null data");
assert_eq!(
runtime.live_instance_count_for_contracts(&[cid]),
0,
"stateless instances are not counted"
);
unsafe { host_destroy_guest_instance(host, interface, inst) };
assert_eq!(runtime.live_instance_count_for_contracts(&[cid]), 0);
}
#[test]
fn current_init_bundle_id_zero_outside_window() {
let runtime: Arc<Runtime> = Runtime::builder().build().expect("build");
assert_eq!(
runtime.active_init_count.load(Ordering::Relaxed),
0,
"fresh runtime has no active init windows"
);
assert_eq!(runtime.current_init_bundle_id(), 0);
}
#[test]
fn current_init_bundle_id_tracks_nested_push_pop() {
let runtime: Arc<Runtime> = Runtime::builder().build().expect("build");
runtime.push_init_bundle_id(0xAAAA);
assert_eq!(runtime.active_init_count.load(Ordering::Relaxed), 1);
assert_eq!(runtime.current_init_bundle_id(), 0xAAAA);
runtime.push_init_bundle_id(0xBBBB);
assert_eq!(runtime.active_init_count.load(Ordering::Relaxed), 2);
assert_eq!(runtime.current_init_bundle_id(), 0xBBBB);
runtime.pop_init_bundle_id();
assert_eq!(runtime.active_init_count.load(Ordering::Relaxed), 1);
assert_eq!(runtime.current_init_bundle_id(), 0xAAAA);
runtime.pop_init_bundle_id();
assert_eq!(
runtime.active_init_count.load(Ordering::Relaxed),
0,
"counter must return to 0 after balanced push/pop"
);
assert_eq!(runtime.current_init_bundle_id(), 0);
}
#[test]
fn pop_without_push_does_not_underflow_counter() {
let runtime: Arc<Runtime> = Runtime::builder().build().expect("build");
runtime.pop_init_bundle_id();
assert_eq!(
runtime.active_init_count.load(Ordering::Relaxed),
0,
"pop with no entry must not decrement the counter"
);
assert_eq!(runtime.current_init_bundle_id(), 0);
}
struct EnforceLoader {
contract_id: u64,
error_bundle_id: u64,
}
impl crate::loader::BundleLoader for EnforceLoader {
fn loader_name(&self) -> &'static str {
"enforce"
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
false
}
fn load(
&self,
_manifest: &ManifestData,
_source: &crate::loader::BundleSource,
runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
runtime.push_init_bundle_id(self.error_bundle_id);
runtime.pop_init_bundle_id();
Err(crate::error::LoaderError::InitFailed {
bundle: "enforce".to_owned(),
error: format!(
"undeclared dependency: bundle_id={:#x} contract_id={:#x}",
self.error_bundle_id, self.contract_id
),
})
}
fn reload(
&self,
_manifest: &ManifestData,
_runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
Err(crate::error::LoaderError::HotReloadUnsupported {
loader_name: self.loader_name().to_owned(),
})
}
}
struct ProbeLoader {
observed_init: Arc<std::sync::Mutex<Option<bool>>>,
}
impl crate::loader::BundleLoader for ProbeLoader {
fn loader_name(&self) -> &'static str {
"probe"
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
false
}
fn load(
&self,
_manifest: &ManifestData,
_source: &crate::loader::BundleSource,
_runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
let mut guard: std::sync::MutexGuard<'_, Option<bool>> = match self.observed_init.lock()
{
Ok(g) => g,
Err(e) => e.into_inner(),
};
*guard = Some(true);
Ok(())
}
fn reload(
&self,
_manifest: &ManifestData,
_runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
Err(crate::error::LoaderError::HotReloadUnsupported {
loader_name: self.loader_name().to_owned(),
})
}
}
struct PanicLoader;
impl crate::loader::BundleLoader for PanicLoader {
fn loader_name(&self) -> &'static str {
"panic"
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
false
}
fn load(
&self,
_manifest: &ManifestData,
_source: &crate::loader::BundleSource,
_runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
panic!("intentional panic in PanicLoader");
}
fn reload(
&self,
_manifest: &ManifestData,
_runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
Err(crate::error::LoaderError::HotReloadUnsupported {
loader_name: self.loader_name().to_owned(),
})
}
}
struct ReentrantState {
runtime_ptr: usize,
inner_bundle: PathBuf,
inner_load_completed: Option<bool>,
}
struct ReentrantLoader {
state: Arc<std::sync::Mutex<ReentrantState>>,
}
impl crate::loader::BundleLoader for ReentrantLoader {
fn loader_name(&self) -> &'static str {
"reentrant"
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
false
}
fn load(
&self,
_manifest: &ManifestData,
_source: &crate::loader::BundleSource,
_runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
let state: std::sync::MutexGuard<'_, ReentrantState> = match self.state.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
let runtime_ptr: usize = state.runtime_ptr;
if runtime_ptr == 0 {
return Err(crate::error::LoaderError::InitFailed {
bundle: "reentrant".to_owned(),
error: "runtime pointer not initialized".to_owned(),
});
}
let inner_bundle: PathBuf = state.inner_bundle.clone();
let already_set: bool = state.inner_load_completed.is_some();
drop(state);
let runtime_ref: &Runtime = unsafe { &*(runtime_ptr as *const Runtime) };
let inner_result: Result<(), crate::error::RuntimeError> = runtime_ref
.load_bundle_with(
inner_bundle.as_path(),
LoadOptions {
compatibility: polyplug_abi::runtime::Compatibility::default(),
ignore_function_count_mismatch: false,
},
);
if let Err(e) = inner_result {
return Err(crate::error::LoaderError::InitFailed {
bundle: "reentrant".to_owned(),
error: e.to_string(),
});
}
let mut st2: std::sync::MutexGuard<'_, ReentrantState> = match self.state.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
if !already_set {
st2.inner_load_completed = Some(true);
}
Ok(())
}
fn reload(
&self,
_manifest: &ManifestData,
_runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
Err(crate::error::LoaderError::HotReloadUnsupported {
loader_name: self.loader_name().to_owned(),
})
}
}
struct LazyState {
observed_init: Option<bool>,
}
struct LazyLoader {
state: Arc<std::sync::Mutex<LazyState>>,
}
impl crate::loader::BundleLoader for LazyLoader {
fn loader_name(&self) -> &'static str {
"lazy"
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
false
}
fn load(
&self,
_manifest: &ManifestData,
_source: &crate::loader::BundleSource,
_runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
let mut state: std::sync::MutexGuard<'_, LazyState> = match self.state.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
if state.observed_init.is_none() {
state.observed_init = Some(true);
}
Ok(())
}
fn reload(
&self,
_manifest: &ManifestData,
_runtime: &Runtime,
) -> Result<(), crate::error::LoaderError> {
Err(crate::error::LoaderError::HotReloadUnsupported {
loader_name: self.loader_name().to_owned(),
})
}
}
#[test]
fn bundle_id_zero_escape_returns_undeclared_dependency_error() {
let temp: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(t) => t,
Err(e) => panic!("failed to create temp dir: {e}"),
};
let contract: u64 = polyplug_utils::guest_contract_id("trust.test", 1_u32);
let bundle_name: &str = "enforce_bundle";
let bundle_path: PathBuf = create_bundle_dir(&temp, bundle_name, "enforce");
let runtime: Arc<Runtime> = match Runtime::builder()
.loader(EnforceLoader {
contract_id: contract,
error_bundle_id: 0_u64,
})
.build()
{
Ok(rt) => rt,
Err(e) => panic!("failed to build runtime: {e}"),
};
let registry: &Arc<RuntimeStore> = runtime.registry();
let _handle: GuestContractHandle =
register_guest_contract(registry.as_ref(), contract, 0xBEEF_u64);
let result: Result<(), crate::error::RuntimeError> =
runtime.load_bundle(bundle_path.as_path());
match result {
Err(RuntimeError::Loader(crate::error::LoaderError::InitFailed {
bundle: _,
error,
})) => {
assert!(error.contains("undeclared dependency"), "got: {error}");
assert!(error.contains("0x0"), "bundle_id zero escape: {error}");
assert!(
error.contains(&format!("{contract:#x}")),
"contract id in message: {error}"
);
}
Err(other) => panic!("unexpected error: {other}"),
Ok(()) => panic!("expected undeclared dependency error"),
}
}
#[test]
fn tls_state_cleared_after_init_completes() {
let temp: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(t) => t,
Err(e) => panic!("failed to create temp dir: {e}"),
};
let contract: u64 = polyplug_utils::guest_contract_id("trust.tls", 1_u32);
let observed: Arc<std::sync::Mutex<Option<bool>>> = Arc::new(std::sync::Mutex::new(None));
let bundle_path: PathBuf = create_bundle_dir(&temp, "probe_bundle", "probe");
let runtime: Arc<Runtime> = match Runtime::builder()
.loader(ProbeLoader {
observed_init: Arc::clone(&observed),
})
.build()
{
Ok(rt) => rt,
Err(e) => panic!("failed to build runtime: {e}"),
};
let registry: &Arc<RuntimeStore> = runtime.registry();
let _handle: GuestContractHandle =
register_guest_contract(registry.as_ref(), contract, 0xCAFE_u64);
let result: Result<(), crate::error::RuntimeError> =
runtime.load_bundle(bundle_path.as_path());
if let Err(e) = result {
panic!("load_bundle failed: {e}");
}
let observed_value: Option<bool> = match observed.lock() {
Ok(g) => *g,
Err(e) => *e.into_inner(),
};
assert_eq!(
observed_value,
Some(true),
"loader should have been called during init"
);
let handle_after: Result<GuestContractHandle, _> =
runtime.find_guest_contract(contract, 0_u32);
assert!(
handle_after.is_ok(),
"after init, find_guest_contract should succeed"
);
}
#[test]
fn panic_during_init_is_caught() {
let temp: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(t) => t,
Err(e) => panic!("failed to create temp dir: {e}"),
};
let _bundle_root: PathBuf = create_bundle_dir(&temp, "panic_bundle", "panic");
let plugin_dir: PathBuf = temp.path().to_path_buf();
let result = std::panic::catch_unwind(|| {
let _rt: Arc<Runtime> = Runtime::builder()
.plugin_dir(plugin_dir)
.loader(PanicLoader)
.build()
.unwrap_or_else(|e| panic!("runtime build failed: {e}"));
});
if result.is_ok() {
panic!("expected panic from PanicLoader");
}
}
#[test]
fn reentrant_load_on_same_thread_works() {
let temp: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(t) => t,
Err(e) => panic!("failed to create temp dir: {e}"),
};
let contract: u64 = polyplug_utils::guest_contract_id("trust.reentrant", 1_u32);
let outer_bundle: PathBuf = create_bundle_dir(&temp, "outer_bundle", "reentrant");
let inner_bundle: PathBuf = create_bundle_dir(&temp, "inner_bundle", "probe");
let state: Arc<std::sync::Mutex<ReentrantState>> =
Arc::new(std::sync::Mutex::new(ReentrantState {
runtime_ptr: 0,
inner_bundle: inner_bundle.clone(),
inner_load_completed: None,
}));
let runtime: Arc<Runtime> = match Runtime::builder()
.loader(ReentrantLoader {
state: Arc::clone(&state),
})
.loader(ProbeLoader {
observed_init: Arc::new(std::sync::Mutex::new(None)),
})
.build()
{
Ok(rt) => rt,
Err(e) => panic!("failed to build runtime: {e}"),
};
let registry: &Arc<RuntimeStore> = runtime.registry();
let _handle: GuestContractHandle =
register_guest_contract(registry.as_ref(), contract, 0xABCD_u64);
{
let mut guard: std::sync::MutexGuard<'_, ReentrantState> = match state.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
guard.runtime_ptr = Arc::as_ptr(&runtime) as usize;
}
let result: Result<(), crate::error::RuntimeError> = runtime.load_bundle_with(
outer_bundle.as_path(),
LoadOptions {
compatibility: polyplug_abi::runtime::Compatibility::default(),
ignore_function_count_mismatch: false,
},
);
if let Err(e) = result {
panic!("outer load failed: {e}");
}
let inner_completed: Option<bool> = match state.lock() {
Ok(g) => g.inner_load_completed,
Err(e) => e.into_inner().inner_load_completed,
};
assert_eq!(
inner_completed,
Some(true),
"inner load should have completed successfully"
);
let _ = inner_bundle;
}
#[test]
fn lazy_load_during_init_works() {
let temp: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(t) => t,
Err(e) => panic!("failed to create temp dir: {e}"),
};
let contract: u64 = polyplug_utils::guest_contract_id("trust.lazy", 1_u32);
let outer_bundle: PathBuf = create_bundle_dir(&temp, "lazy_outer", "lazy");
let inner_bundle: PathBuf = create_bundle_dir(&temp, "lazy_inner", "probe");
let state: Arc<std::sync::Mutex<LazyState>> = Arc::new(std::sync::Mutex::new(LazyState {
observed_init: None,
}));
let runtime: Arc<Runtime> = match Runtime::builder()
.loader(LazyLoader {
state: Arc::clone(&state),
})
.loader(ProbeLoader {
observed_init: Arc::new(std::sync::Mutex::new(None)),
})
.build()
{
Ok(rt) => rt,
Err(e) => panic!("failed to build runtime: {e}"),
};
let registry: &Arc<RuntimeStore> = runtime.registry();
let _handle: GuestContractHandle =
register_guest_contract(registry.as_ref(), contract, 0xFACE_u64);
let result: Result<(), crate::error::RuntimeError> =
runtime.load_bundle(outer_bundle.as_path());
if let Err(e) = result {
panic!("outer load failed: {e}");
}
let observed_init: Option<bool> = match state.lock() {
Ok(g) => g.observed_init,
Err(e) => e.into_inner().observed_init,
};
assert_eq!(
observed_init,
Some(true),
"init should have been observed during lazy loader init"
);
let inner_result: Result<(), crate::error::RuntimeError> = runtime.load_bundle_with(
inner_bundle.as_path(),
LoadOptions {
compatibility: polyplug_abi::runtime::Compatibility::default(),
ignore_function_count_mismatch: false,
},
);
if let Err(e) = inner_result {
panic!("lazy inner load failed: {e}");
}
}
fn create_host_contract_interface(
contract_id: u64,
major: u32,
minor: u32,
) -> &'static HostContractInterface {
use polyplug_abi::{
DispatchMechanisms, DispatchType, HostContractInstance, NativeDispatch,
};
unsafe extern "C" fn stub_create_instance(
_this: *const HostContractInterface,
_args: *const (),
out_instance: *mut HostContractInstance,
) {
static mut DUMMY: usize = 0xDEADBEEF;
if !out_instance.is_null() {
unsafe {
out_instance.write(HostContractInstance {
data: &raw mut DUMMY as *mut core::ffi::c_void,
})
};
}
}
unsafe extern "C" fn stub_destroy_instance(
_this: *const HostContractInterface,
_instance: HostContractInstance,
) {
}
Box::leak(Box::new(HostContractInterface {
contract_id: polyplug_utils::HostContractId::from(contract_id),
contract_version: polyplug_abi::types::Version {
major,
minor,
patch: 0,
},
singleton: true,
dispatch_type: DispatchType::Native,
runtime: core::ptr::null_mut(),
user_data: core::ptr::null_mut(),
create_instance: stub_create_instance,
destroy_instance: stub_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 0,
functions: core::ptr::null(),
},
},
}))
}
#[test]
fn runtime_host_contracts_register_guest_contract_and_lookup() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let contract_id: u64 = polyplug_utils::host_contract_id("host.logger", 1);
let interface: &'static HostContractInterface =
create_host_contract_interface(contract_id, 1, 0);
let result: Result<(), HostContractError> =
runtime.register_host_contract(contract_id, interface);
assert!(result.is_ok(), "registration should succeed");
let found: Option<&'static HostContractInterface> =
runtime.get_host_contract(contract_id, 0);
assert!(found.is_some(), "contract should be found");
let found_interface: &HostContractInterface =
found.expect("contract should be present after is_some check");
assert_eq!(found_interface.contract_id.id(), contract_id);
}
#[test]
fn runtime_host_contracts_duplicate_registration_fails() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let contract_id: u64 = polyplug_utils::host_contract_id("host.logger", 1);
let interface1: &'static HostContractInterface =
create_host_contract_interface(contract_id, 1, 0);
let interface2: &'static HostContractInterface =
create_host_contract_interface(contract_id, 1, 1);
let result1: Result<(), HostContractError> =
runtime.register_host_contract(contract_id, interface1);
assert!(result1.is_ok(), "first registration should succeed");
let result2: Result<(), HostContractError> =
runtime.register_host_contract(contract_id, interface2);
assert!(result2.is_err(), "duplicate registration should fail");
match result2 {
Err(HostContractError::DuplicateContract { contract_id: id }) => {
assert_eq!(id, contract_id);
}
Err(other) => panic!("unexpected error: {other}"),
Ok(()) => panic!("expected error"),
}
}
#[test]
fn runtime_host_contracts_unregister_guest_contract() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let contract_id: u64 = polyplug_utils::host_contract_id("host.logger", 1);
let interface: &'static HostContractInterface =
create_host_contract_interface(contract_id, 1, 0);
runtime
.register_host_contract(contract_id, interface)
.expect("registration should succeed");
let removed: bool = runtime.unregister_host_contract(contract_id);
assert!(
removed,
"unregister_guest_contract should return true for existing contract"
);
let removed_again: bool = runtime.unregister_host_contract(contract_id);
assert!(
!removed_again,
"unregister_guest_contract should return false for non-existent contract"
);
let found: Option<&'static HostContractInterface> =
runtime.get_host_contract(contract_id, 0);
assert!(
found.is_none(),
"contract should not be found after unregister_guest_contract"
);
}
#[test]
fn runtime_host_contracts_version_check() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let contract_id: u64 = polyplug_utils::host_contract_id("host.logger", 2);
let interface: &'static HostContractInterface =
create_host_contract_interface(contract_id, 2, 5);
runtime
.register_host_contract(contract_id, interface)
.expect("registration should succeed");
let found_low: Option<&'static HostContractInterface> =
runtime.get_host_contract(contract_id, 0);
assert!(found_low.is_some(), "should find with min_version=0");
let found_exact: Option<&'static HostContractInterface> =
runtime.get_host_contract(contract_id, (2 << 16) | 5);
assert!(found_exact.is_some(), "should find with exact version");
let found_higher_minor: Option<&'static HostContractInterface> =
runtime.get_host_contract(contract_id, (2 << 16) | 3);
assert!(
found_higher_minor.is_some(),
"should find with lower minor version requirement"
);
let found_higher_major: Option<&'static HostContractInterface> =
runtime.get_host_contract(contract_id, 3 << 16);
assert!(
found_higher_major.is_none(),
"should not find with higher major version requirement"
);
}
#[test]
fn runtime_host_language_default_is_rust() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
assert_eq!(runtime.host_language(), SupportedLanguage::Rust);
}
#[test]
fn runtime_host_language_can_be_set() {
let runtime: Arc<Runtime> = Runtime::builder()
.host_language(SupportedLanguage::Python)
.build()
.expect("runtime build should succeed");
assert_eq!(runtime.host_language(), SupportedLanguage::Python);
}
#[test]
fn host_get_host_contract_callback_returns_register_guest_contracted_contract() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let contract_id: u64 = polyplug_utils::host_contract_id("host.test", 1);
let interface: &'static HostContractInterface =
create_host_contract_interface(contract_id, 1, 0);
runtime
.register_host_contract(contract_id, interface)
.expect("registration should succeed");
let host_interface: HostApi = HostApi {
runtime: Arc::as_ptr(&runtime) as *mut core::ffi::c_void,
register_guest_contract: host_register_guest_contract,
alloc: host_alloc,
free: host_free,
find_guest_contract: host_find_guest_contract,
find_all_guest_contracts: host_find_all_guest_contracts,
resolve_guest_contract: host_resolve_guest_contract,
get_host_contract: host_get_host_contract,
resolve_host_contract_interface: host_resolve_host_contract_interface,
list_bundles: host_list_bundles,
get_dependencies: host_get_dependencies,
load_bundle: host_load_bundle,
reload_bundle: host_reload_bundle,
register_host_contract: host_register_host_contract,
register_loader: host_register_loader,
get_last_error: host_get_last_error,
get_error_len: host_get_error_len,
unload_bundle: host_unload_bundle,
log: stub_host_log,
create_guest_instance: host_create_guest_instance,
destroy_guest_instance: host_destroy_guest_instance,
revision_counter: host_revision_counter,
reserved: core::ptr::null(),
};
let instance: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, contract_id, 0) };
assert!(
!instance.data.is_null(),
"callback should return non-null instance for register_guest_contracted contract"
);
}
#[test]
fn host_get_host_contract_callback_returns_null_for_unregister_guest_contracted() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let contract_id: u64 = polyplug_utils::host_contract_id("host.nonexistent", 1);
let host_interface: HostApi = HostApi {
runtime: Arc::as_ptr(&runtime) as *mut core::ffi::c_void,
register_guest_contract: host_register_guest_contract,
alloc: host_alloc,
free: host_free,
find_guest_contract: host_find_guest_contract,
find_all_guest_contracts: host_find_all_guest_contracts,
resolve_guest_contract: host_resolve_guest_contract,
get_host_contract: host_get_host_contract,
resolve_host_contract_interface: host_resolve_host_contract_interface,
list_bundles: host_list_bundles,
get_dependencies: host_get_dependencies,
load_bundle: host_load_bundle,
reload_bundle: host_reload_bundle,
register_host_contract: host_register_host_contract,
register_loader: host_register_loader,
get_last_error: host_get_last_error,
get_error_len: host_get_error_len,
unload_bundle: host_unload_bundle,
log: stub_host_log,
create_guest_instance: host_create_guest_instance,
destroy_guest_instance: host_destroy_guest_instance,
revision_counter: host_revision_counter,
reserved: core::ptr::null(),
};
let instance: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, contract_id, 0) };
assert!(
instance.data.is_null(),
"callback should return null instance for unregister_guest_contracted contract"
);
}
std::thread_local! {
static LOCAL_INSTANCE_COUNTER: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
unsafe extern "C" fn counting_create_instance(
_this: *const HostContractInterface,
_args: *const (),
out_instance: *mut HostContractInstance,
) {
let instance: HostContractInstance = LOCAL_INSTANCE_COUNTER.with(|counter| {
let count: usize = counter.get();
counter.set(count + 1);
HostContractInstance {
data: (count + 1) as *mut core::ffi::c_void, }
});
if !out_instance.is_null() {
unsafe { out_instance.write(instance) };
}
}
unsafe extern "C" fn counting_destroy_instance(
_this: *const HostContractInterface,
_instance: HostContractInstance,
) {
}
fn create_counting_host_contract_interface(
contract_id: u64,
major: u32,
singleton: bool,
) -> &'static HostContractInterface {
use polyplug_abi::{DispatchMechanisms, DispatchType, NativeDispatch};
Box::leak(Box::new(HostContractInterface {
contract_id: polyplug_utils::HostContractId::from(contract_id),
contract_version: polyplug_abi::types::Version {
major,
minor: 0,
patch: 0,
},
singleton,
dispatch_type: DispatchType::Native,
runtime: core::ptr::null_mut(),
user_data: core::ptr::null_mut(),
create_instance: counting_create_instance,
destroy_instance: counting_destroy_instance,
dispatch: DispatchMechanisms {
native: NativeDispatch {
function_count: 0,
functions: core::ptr::null(),
},
},
}))
}
#[test]
fn singleton_contract_returns_cached_instance_on_multiple_calls() {
LOCAL_INSTANCE_COUNTER.with(|counter| counter.set(0));
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let contract_id: u64 = polyplug_utils::host_contract_id("singleton.test", 1);
let interface: &'static HostContractInterface =
create_counting_host_contract_interface(contract_id, 1, true);
runtime
.register_host_contract(contract_id, interface)
.expect("registration should succeed");
let host_interface: HostApi = HostApi {
runtime: Arc::as_ptr(&runtime) as *mut core::ffi::c_void,
register_guest_contract: host_register_guest_contract,
alloc: host_alloc,
free: host_free,
find_guest_contract: host_find_guest_contract,
find_all_guest_contracts: host_find_all_guest_contracts,
resolve_guest_contract: host_resolve_guest_contract,
get_host_contract: host_get_host_contract,
resolve_host_contract_interface: host_resolve_host_contract_interface,
list_bundles: host_list_bundles,
get_dependencies: host_get_dependencies,
load_bundle: host_load_bundle,
reload_bundle: host_reload_bundle,
register_host_contract: host_register_host_contract,
register_loader: host_register_loader,
get_last_error: host_get_last_error,
get_error_len: host_get_error_len,
unload_bundle: host_unload_bundle,
log: stub_host_log,
create_guest_instance: host_create_guest_instance,
destroy_guest_instance: host_destroy_guest_instance,
revision_counter: host_revision_counter,
reserved: core::ptr::null(),
};
let instance1: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, contract_id, 0) };
assert!(
!instance1.data.is_null(),
"first call should return non-null instance"
);
let instance2: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, contract_id, 0) };
assert!(
!instance2.data.is_null(),
"second call should return non-null instance"
);
assert_eq!(
instance1.data, instance2.data,
"singleton contract should return cached instance (same pointer)"
);
let counter_value: usize = LOCAL_INSTANCE_COUNTER.with(|counter| counter.get());
assert_eq!(
counter_value, 1,
"singleton should only call create_instance once"
);
let instance3: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, contract_id, 0) };
assert_eq!(
instance1.data, instance3.data,
"third call should still return same cached instance"
);
assert_eq!(
LOCAL_INSTANCE_COUNTER.with(|counter| counter.get()),
1,
"counter still at 1 - no additional create calls"
);
}
#[test]
fn multi_instance_contract_creates_new_instance_on_each_call() {
LOCAL_INSTANCE_COUNTER.with(|counter| counter.set(100));
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let contract_id: u64 = polyplug_utils::host_contract_id("multi.test", 1);
let interface: &'static HostContractInterface =
create_counting_host_contract_interface(contract_id, 1, false);
runtime
.register_host_contract(contract_id, interface)
.expect("registration should succeed");
let host_interface: HostApi = HostApi {
runtime: Arc::as_ptr(&runtime) as *mut core::ffi::c_void,
register_guest_contract: host_register_guest_contract,
alloc: host_alloc,
free: host_free,
find_guest_contract: host_find_guest_contract,
find_all_guest_contracts: host_find_all_guest_contracts,
resolve_guest_contract: host_resolve_guest_contract,
get_host_contract: host_get_host_contract,
resolve_host_contract_interface: host_resolve_host_contract_interface,
list_bundles: host_list_bundles,
get_dependencies: host_get_dependencies,
load_bundle: host_load_bundle,
reload_bundle: host_reload_bundle,
register_host_contract: host_register_host_contract,
register_loader: host_register_loader,
get_last_error: host_get_last_error,
get_error_len: host_get_error_len,
unload_bundle: host_unload_bundle,
log: stub_host_log,
create_guest_instance: host_create_guest_instance,
destroy_guest_instance: host_destroy_guest_instance,
revision_counter: host_revision_counter,
reserved: core::ptr::null(),
};
let instance1: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, contract_id, 0) };
assert!(
!instance1.data.is_null(),
"first call should return non-null instance"
);
let instance2: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, contract_id, 0) };
assert!(
!instance2.data.is_null(),
"second call should return non-null instance"
);
assert_ne!(
instance1.data, instance2.data,
"multi-instance contract should create new instance each call (different pointers)"
);
let counter_value: usize = LOCAL_INSTANCE_COUNTER.with(|counter| counter.get());
assert_eq!(
counter_value, 102,
"multi-instance should call create_instance twice"
);
let instance3: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, contract_id, 0) };
assert_ne!(
instance1.data, instance3.data,
"third instance differs from first"
);
assert_ne!(
instance2.data, instance3.data,
"third instance differs from second"
);
assert_eq!(
LOCAL_INSTANCE_COUNTER.with(|counter| counter.get()),
103,
"counter at 103 - three create calls"
);
}
#[test]
fn singleton_and_multi_instance_contracts_coexist() {
LOCAL_INSTANCE_COUNTER.with(|counter| counter.set(0));
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let singleton_id: u64 = polyplug_utils::host_contract_id("singleton.mixed", 1);
let multi_id: u64 = polyplug_utils::host_contract_id("multi.mixed", 1);
let singleton_interface: &'static HostContractInterface =
create_counting_host_contract_interface(singleton_id, 1, true);
let multi_interface: &'static HostContractInterface =
create_counting_host_contract_interface(multi_id, 1, false);
runtime
.register_host_contract(singleton_id, singleton_interface)
.expect("singleton registration should succeed");
runtime
.register_host_contract(multi_id, multi_interface)
.expect("multi-instance registration should succeed");
let host_interface: HostApi = HostApi {
runtime: Arc::as_ptr(&runtime) as *mut core::ffi::c_void,
register_guest_contract: host_register_guest_contract,
alloc: host_alloc,
free: host_free,
find_guest_contract: host_find_guest_contract,
find_all_guest_contracts: host_find_all_guest_contracts,
resolve_guest_contract: host_resolve_guest_contract,
get_host_contract: host_get_host_contract,
resolve_host_contract_interface: host_resolve_host_contract_interface,
list_bundles: host_list_bundles,
get_dependencies: host_get_dependencies,
load_bundle: host_load_bundle,
reload_bundle: host_reload_bundle,
register_host_contract: host_register_host_contract,
register_loader: host_register_loader,
get_last_error: host_get_last_error,
get_error_len: host_get_error_len,
unload_bundle: host_unload_bundle,
log: stub_host_log,
create_guest_instance: host_create_guest_instance,
destroy_guest_instance: host_destroy_guest_instance,
revision_counter: host_revision_counter,
reserved: core::ptr::null(),
};
let s1: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, singleton_id, 0) };
let s2: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, singleton_id, 0) };
assert_eq!(s1.data, s2.data, "singleton returns cached instance");
let m1: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, multi_id, 0) };
let m2: HostContractInstance =
unsafe { host_get_host_contract(&host_interface as *const HostApi, multi_id, 0) };
assert_ne!(m1.data, m2.data, "multi-instance returns new instances");
assert_ne!(
s1.data, m1.data,
"singleton and multi instances are different"
);
assert_ne!(
s1.data, m2.data,
"singleton and multi instances are different"
);
}
#[test]
fn unload_refuses_provider_with_dependent_then_cascade_succeeds() {
let runtime: Arc<Runtime> = Runtime::builder()
.build()
.expect("runtime build should succeed");
let provider_contract_id: u64 = 0x0BAD_F00D_0000_00A1;
let provider_bundle_id: BundleId = BundleId::from_u64(0xA);
let dependent_bundle_id: BundleId = BundleId::from_u64(0xB);
register_native_caller_contract(&runtime.registry, provider_contract_id, 0xA);
register_native_caller_contract(&runtime.registry, 0x0BAD_F00D_0000_00B2, 0xB);
runtime
.registry
.register_bundle_metadata(
provider_bundle_id,
"bundle_a".to_owned(),
Version {
major: 1,
minor: 0,
patch: 0,
},
SupportedLanguage::Rust,
PathBuf::new(),
Vec::new(),
)
.expect("provider metadata registration should succeed");
runtime
.registry
.register_bundle_metadata(
dependent_bundle_id,
"bundle_b".to_owned(),
Version {
major: 1,
minor: 0,
patch: 0,
},
SupportedLanguage::Rust,
PathBuf::new(),
Vec::new(),
)
.expect("dependent metadata registration should succeed");
runtime
.registry
.declare_bundle_dependencies(
dependent_bundle_id,
vec![GuestContractId::from_u64(provider_contract_id)],
)
.expect("dependency declaration should succeed");
match runtime.unload_bundle(provider_bundle_id) {
Err(RuntimeError::DependencyInUse {
provider,
dependents,
}) => {
assert_eq!(provider, "bundle_a");
assert_eq!(dependents, vec!["bundle_b".to_owned()]);
}
other => panic!("expected DependencyInUse refusal, got {other:?}"),
}
runtime
.unload_bundle_cascade(provider_bundle_id)
.expect("cascade unload should succeed");
assert!(
runtime
.registry
.get_bundle_descriptor(provider_bundle_id)
.is_none(),
"provider bundle must be gone after cascade unload"
);
assert!(
runtime
.registry
.get_bundle_descriptor(dependent_bundle_id)
.is_none(),
"dependent bundle must be gone after cascade unload"
);
}
}