use core::sync::atomic::AtomicU64;
use core::sync::atomic::Ordering;
use std::collections::HashMap;
use std::sync::Mutex;
use pyo3::Bound;
use pyo3::Py;
use pyo3::PyAny;
use pyo3::Python;
use pyo3::types::PyAnyMethods;
use pyo3::types::PyList;
use pyo3::types::PyListMethods;
use pyo3::types::PyTuple;
use pyo3::types::PyTupleMethods;
use polyplug::error::LoaderError;
use polyplug_abi::AbiError;
use polyplug_abi::AbiErrorCode;
use polyplug_abi::CallArena;
use polyplug_abi::DispatchType;
use polyplug_abi::GuestContractInstance;
use polyplug_abi::GuestContractInterface;
use polyplug_abi::HostApi;
use polyplug_abi::PluginDescriptor;
use polyplug_abi::StringView;
use polyplug_abi::VmLoaderData;
use polyplug_abi::dispatch::dispatch_mechanisms::DispatchMechanisms;
use polyplug_abi::dispatch::vm_dispatch::VmDispatch;
use polyplug_abi::types::Version;
use polyplug_utils::GuestContractId;
pub struct PythonLoaderData {
pub callables: Vec<Py<PyAny>>,
pub arena_alloc: Py<PyAny>,
pub factory: Py<PyAny>,
pub default_impl: Py<PyAny>,
pub instances: Mutex<HashMap<u64, Py<PyAny>>>,
pub next_id: AtomicU64,
pub contract_id: GuestContractId,
}
unsafe impl Send for PythonLoaderData {}
unsafe impl Sync for PythonLoaderData {}
unsafe extern "C" fn python_create_instance(
loader_data: VmLoaderData,
host: *const HostApi,
_args: *const (),
out_instance: *mut GuestContractInstance,
) {
if out_instance.is_null() {
return;
}
let data: &PythonLoaderData = unsafe { &*(loader_data.data as *const PythonLoaderData) };
let host_addr: i64 = host as usize as i64;
let instance: GuestContractInstance =
Python::attach(
|py: Python<'_>| match data.factory.bind(py).call1((host_addr,)) {
Ok(impl_obj) => {
let id: u64 = data.next_id.fetch_add(1, Ordering::Relaxed);
match data.instances.lock() {
Ok(mut map) => {
map.insert(id, impl_obj.unbind());
GuestContractInstance {
data: id as usize as *mut core::ffi::c_void,
contract_id: data.contract_id,
}
}
Err(_) => GuestContractInstance::null(),
}
}
Err(e) => {
e.print(py);
GuestContractInstance::null()
}
},
);
unsafe { out_instance.write(instance) };
}
unsafe extern "C" fn python_destroy_instance(
loader_data: VmLoaderData,
_host: *const HostApi,
instance: GuestContractInstance,
) {
let id: u64 = instance.data as usize as u64;
if id == 0 {
return;
}
let data: &PythonLoaderData = unsafe { &*(loader_data.data as *const PythonLoaderData) };
Python::attach(|_py: Python<'_>| {
if let Ok(mut map) = data.instances.lock() {
map.remove(&id);
}
});
}
unsafe extern "C" fn python_vm_dispatch(
loader_data: VmLoaderData,
instance: GuestContractInstance,
fn_id: u32,
args: *const (),
out: *mut (),
arena: *mut CallArena,
out_err: *mut AbiError,
) {
let result: AbiError =
unsafe { python_vm_dispatch_impl(loader_data, instance, fn_id, args, out, arena) };
if !out_err.is_null() {
unsafe { out_err.write(result) };
}
}
unsafe fn python_vm_dispatch_impl(
loader_data: VmLoaderData,
instance: GuestContractInstance,
fn_id: u32,
args: *const (),
out: *mut (),
arena: *mut CallArena,
) -> AbiError {
let data: &PythonLoaderData = unsafe { &*(loader_data.data as *const PythonLoaderData) };
let callable: &Py<PyAny> = match data.callables.get(fn_id as usize) {
Some(c) => c,
None => {
return AbiError {
code: AbiErrorCode::FunctionNotAvailable as u32,
message: StringView::null(),
};
}
};
let instance_id: u64 = instance.data as usize as u64;
let args_int: i64 = args as usize as i64;
let out_int: i64 = out as usize as i64;
let arena_int: i64 = arena as usize as i64;
Python::attach(|py: Python<'_>| {
let impl_py: Py<PyAny> = if instance_id == 0 {
data.default_impl.clone_ref(py)
} else {
let map: std::sync::MutexGuard<'_, HashMap<u64, Py<PyAny>>> =
match data.instances.lock() {
Ok(g) => g,
Err(_) => {
return AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
};
}
};
match map.get(&instance_id) {
Some(obj) => obj.clone_ref(py),
None => {
return AbiError {
code: AbiErrorCode::FunctionNotAvailable as u32,
message: StringView::null(),
};
}
}
};
let arena_alloc: Bound<'_, PyAny> = data.arena_alloc.bind(py).clone();
let bound: Bound<'_, PyAny> = callable.bind(py).clone();
let call_result: Result<Bound<'_, PyAny>, pyo3::PyErr> =
bound.call((impl_py, args_int, out_int, arena_int, arena_alloc), None);
match call_result {
Ok(_) => AbiError::ok(),
Err(e) => {
e.print(py);
AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
}
}
}
})
}
pub(crate) struct ContractRegistration {
pub contract_name: String,
pub contract_major: u32,
pub plugin_name: String,
pub factory: Py<PyAny>,
pub callables: Vec<Py<PyAny>>,
}
pub(crate) fn parse_contract_string(
contract: &str,
bundle_name: &str,
) -> Result<(String, u32), LoaderError> {
let (name, version_part): (&str, &str) =
contract
.split_once('@')
.ok_or_else(|| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"invalid contract string `{}`: expected `name@major[.minor]`",
contract
),
})?;
if name.is_empty() {
return Err(LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"invalid contract string `{}`: empty contract name",
contract
),
});
}
let major_str: &str = version_part.split('.').next().unwrap_or(version_part);
let major: u32 = major_str
.parse::<u32>()
.map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"invalid contract string `{}`: major version `{}` is not a u32",
contract, major_str
),
})?;
Ok((name.to_owned(), major))
}
pub(crate) fn collect_registrations(
py: Python<'_>,
init_ret: &Bound<'_, PyAny>,
bundle_name: &str,
) -> Result<Vec<ContractRegistration>, LoaderError> {
let tuple: Bound<'_, PyTuple> =
init_ret
.cast::<PyTuple>()
.cloned()
.map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: "polyplug_init must return a (registrations, AbiError) tuple".to_owned(),
})?;
if tuple.len() != 2 {
return Err(LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"polyplug_init must return a 2-tuple (registrations, AbiError), got {} elements",
tuple.len()
),
});
}
let registrations_obj: Bound<'_, PyAny> =
tuple.get_item(0).map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: "polyplug_init return tuple missing registrations element".to_owned(),
})?;
let abi_error_obj: Bound<'_, PyAny> =
tuple.get_item(1).map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: "polyplug_init return tuple missing AbiError element".to_owned(),
})?;
let code: u32 = abi_error_obj
.getattr("code")
.and_then(|c: Bound<'_, PyAny>| c.extract::<u32>())
.map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: "polyplug_init AbiError has no readable u32 `code` field".to_owned(),
})?;
if code != AbiErrorCode::Ok as u32 {
return Err(LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("polyplug_init reported AbiError code {}", code),
});
}
let list: Bound<'_, PyList> =
registrations_obj
.cast_into::<PyList>()
.map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: "polyplug_init registrations must be a list of dicts".to_owned(),
})?;
let mut registrations: Vec<ContractRegistration> = Vec::with_capacity(list.len());
for entry in list.iter() {
let contract_str: String = entry
.get_item("contract")
.and_then(|v: Bound<'_, PyAny>| v.extract::<String>())
.map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: "registration entry missing string `contract` key".to_owned(),
})?;
let (contract_name, contract_major): (String, u32) =
parse_contract_string(&contract_str, bundle_name)?;
let plugin_name: String = match entry.get_item("plugin_name") {
Ok(v) => v
.extract::<String>()
.unwrap_or_else(|_| bundle_name.to_owned()),
Err(_) => bundle_name.to_owned(),
};
let factory: Bound<'_, PyAny> =
entry
.get_item("factory")
.map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"registration entry for `{}` missing `factory` callable",
contract_str
),
})?;
if !factory.is_callable() {
return Err(LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("`factory` for `{}` is not callable", contract_str),
});
}
let functions: Bound<'_, PyAny> =
entry
.get_item("functions")
.map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"registration entry for `{}` missing `functions` list",
contract_str
),
})?;
let functions_list: Bound<'_, PyList> =
functions
.cast_into::<PyList>()
.map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("`functions` for `{}` must be a list", contract_str),
})?;
let mut callables: Vec<Py<PyAny>> = Vec::with_capacity(functions_list.len());
for (idx, callable) in functions_list.iter().enumerate() {
if !callable.is_callable() {
return Err(LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"`functions[{}]` for `{}` is not callable",
idx, contract_str
),
});
}
callables.push(callable.unbind());
}
registrations.push(ContractRegistration {
contract_name,
contract_major,
plugin_name,
factory: factory.unbind(),
callables,
});
}
let _ = py;
Ok(registrations)
}
pub(crate) fn register_contracts(
registrations: Vec<ContractRegistration>,
host_interface: *const HostApi,
bundle_name: &str,
) -> Result<u32, LoaderError> {
let mut registered: u32 = 0_u32;
let arena_alloc: Py<PyAny> = Python::attach(|py: Python<'_>| {
build_arena_bridge(py, host_interface, bundle_name).map(|b: Bound<'_, PyAny>| b.unbind())
})?;
for reg in registrations {
let cid: GuestContractId = GuestContractId::new(®.contract_name, reg.contract_major);
let host_addr: i64 = host_interface as usize as i64;
let default_impl: Py<PyAny> =
Python::attach(
|py: Python<'_>| match reg.factory.bind(py).call1((host_addr,)) {
Ok(o) => Ok(o.unbind()),
Err(e) => {
e.print(py);
Err(())
}
},
)
.map_err(|_| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"factory for `{}@{}` failed while building the default instance",
reg.contract_name, reg.contract_major
),
})?;
let arena_alloc_clone: Py<PyAny> =
Python::attach(|py: Python<'_>| arena_alloc.clone_ref(py));
let loader_data: Box<PythonLoaderData> = Box::new(PythonLoaderData {
callables: reg.callables,
arena_alloc: arena_alloc_clone,
factory: reg.factory,
default_impl,
instances: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
contract_id: cid,
});
let loader_data_ptr: *mut PythonLoaderData = Box::into_raw(loader_data);
let interface: GuestContractInterface = GuestContractInterface {
contract_id: cid,
contract_version: Version {
major: reg.contract_major,
minor: 0,
patch: 0,
},
dispatch_type: DispatchType::VirtualMachine,
create_instance: python_create_instance,
destroy_instance: python_destroy_instance,
dispatch: DispatchMechanisms {
vm: VmDispatch {
call: python_vm_dispatch,
loader_data: VmLoaderData {
data: loader_data_ptr as *mut core::ffi::c_void,
},
},
},
};
let static_interface: *const GuestContractInterface = Box::into_raw(Box::new(interface));
let contract_display_name: String = format!("{}@{}", reg.contract_name, reg.contract_major);
let plugin_name_leaked: &'static str = Box::leak(reg.plugin_name.into_boxed_str());
let contract_name_leaked: &'static str = Box::leak(contract_display_name.into_boxed_str());
let descriptor: PluginDescriptor = PluginDescriptor {
name: StringView {
ptr: plugin_name_leaked.as_ptr(),
len: plugin_name_leaked.len(),
},
contract_name: StringView {
ptr: contract_name_leaked.as_ptr(),
len: contract_name_leaked.len(),
},
version: Version {
major: reg.contract_major,
minor: 0,
patch: 0,
},
};
let mut reg_result: AbiError = AbiError::ok();
unsafe {
((*host_interface).register_guest_contract)(
host_interface,
&descriptor as *const PluginDescriptor,
static_interface,
&mut reg_result,
)
};
if !reg_result.is_ok() {
return Err(LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"register_guest_contract failed for `{}`: code={:?}",
contract_name_leaked, reg_result.code
),
});
}
registered += 1;
}
if registered == 0 {
return Err(LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: "polyplug_init returned no contracts (empty registrations list)".to_owned(),
});
}
Ok(registered)
}
fn build_arena_bridge<'py>(
py: Python<'py>,
host_interface: *const HostApi,
bundle_name: &str,
) -> Result<Bound<'py, PyAny>, LoaderError> {
let host_addr: usize = host_interface as usize;
let closure = move |size: u32, arena_addr: usize| -> i64 {
let arena: *mut CallArena = arena_addr as *mut CallArena;
let ptr: *mut u8 = if arena.is_null() {
let host: *const HostApi = host_addr as *const HostApi;
if host.is_null() {
core::ptr::null_mut()
} else {
unsafe { ((*host).alloc)(host, size as usize, 1) }
}
} else {
unsafe { (*arena).alloc(size as usize, 1) }
};
ptr as usize as i64
};
pyo3::types::PyCFunction::new_closure(
py,
None,
None,
move |args: &Bound<'_, pyo3::types::PyTuple>,
_kwargs: Option<&Bound<'_, pyo3::types::PyDict>>|
-> pyo3::PyResult<i64> {
let size: u32 = args.get_item(0)?.extract::<u32>()?;
let arena_addr: usize = args.get_item(1)?.extract::<usize>()?;
Ok(closure(size, arena_addr))
},
)
.map(|f: Bound<'_, pyo3::types::PyCFunction>| f.into_any())
.map_err(|e: pyo3::PyErr| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("failed to create arena_alloc bridge: {}", e),
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
#[test]
fn parse_contract_string_major_only() {
let (name, major): (String, u32) =
parse_contract_string("calculator@2", "b").expect("parse");
assert_eq!(name, "calculator");
assert_eq!(major, 2);
}
#[test]
fn parse_contract_string_major_minor() {
let (name, major): (String, u32) = parse_contract_string("logger@3.7", "b").expect("parse");
assert_eq!(name, "logger");
assert_eq!(major, 3, "minor is parsed but ignored for the id");
}
#[test]
fn parse_contract_string_missing_at_fails() {
assert!(parse_contract_string("noversion", "b").is_err());
}
#[test]
fn parse_contract_string_empty_name_fails() {
assert!(parse_contract_string("@1", "b").is_err());
}
#[test]
fn parse_contract_string_bad_major_fails() {
assert!(parse_contract_string("x@abc", "b").is_err());
}
}