polyplug_abi 0.1.1

ABI type definitions for the polyplug plugin runtime
//! Guest Contract Interface — one per contract implemented by a guest (plugin).
//!
//! This module defines `GuestContractInterface`, the interface guests register
//! during `polyplug_init()` for each contract they implement.
//!
//! # Who provides
//! Guest (plugin) code creates this struct and registers it via `register_guest_contract`.
//! Must be `'static` or intentionally leaked.
//!
//! # Who calls
//! Host code calls `create_instance`, `destroy_instance`, and dispatch functions.
//!
//! # Ownership
//! Must be `'static`. Never stack-allocated. Never freed while runtime lives.
//! Typically created as `static` or via `Box::leak()`.
//!
//! # Instance Lifecycle
//! - `create_instance`: Factory function to create new instances
//! - `destroy_instance`: Destructor to clean up instances before hot-reload

use polyplug_utils::GuestContractId;

use crate::{
    dispatch::{
        dispatch_mechanisms::DispatchMechanisms, dispatch_type::DispatchType,
        vm_loader_data::VmLoaderData,
    },
    guest::GuestContractInstance,
    host::HostApi,
    types::Version,
};

/// Guest Contract Interface — one per contract implemented by a guest (plugin).
///
/// # Who provides
/// Guest (plugin) code creates this struct and registers it via `register_guest_contract`.
/// Must be `'static` or intentionally leaked.
///
/// # Who calls
/// Host code calls `create_instance`, `destroy_instance`, and dispatch functions.
///
/// # Ownership
/// Must be `'static`. Never stack-allocated. Never freed while runtime lives.
/// Typically created as `static` or via `Box::leak()`.
///
/// # Lifetime
/// Lives for the entire runtime lifetime. Must survive hot-reload.
///
/// # Instance Lifecycle
/// - `create_instance`: Factory function to create new instances
/// - `destroy_instance`: Destructor to clean up instances before hot-reload
///
/// # Dispatch
/// - `dispatch_type == Native`: Call via `dispatch.native.functions[fn_id](instance, args, out, out_err)`
/// - `dispatch_type == VirtualMachine`: Call via `dispatch.vm.call(loader_data, instance, fn_id, args, out, arena, out_err)`
#[repr(C)]
#[derive(Clone, Copy)]
pub struct GuestContractInterface {
    /// FNV-1a hash of "guest_contract:name@major_version".
    ///
    /// Generated by polyplugc from contract name and major version.
    /// Used for contract lookup via `find_guest_contract`.
    pub contract_id: GuestContractId,
    /// Contract version (major, minor, patch).
    ///
    /// Used for version compatibility checks during lookup.
    pub contract_version: Version,
    /// Dispatch mechanism type (Native or VirtualMachine).
    ///
    /// Determines how dispatch functions are invoked:
    /// - Native: Direct function pointer call
    /// - VirtualMachine: VM-specific dispatch via loader
    pub dispatch_type: DispatchType,
    /// Create a new instance of this contract.
    ///
    /// Factory function called by host to create instances.
    /// Returns null handle on failure.
    ///
    /// # Arguments
    /// - `loader_data`: the VM loader's per-(bundle,runtime) data handle (the same
    ///   handle carried in `dispatch.vm.loader_data`). Native-dispatch contracts
    ///   ignore it — their generated factory is statically linked. VM-dispatch
    ///   contracts (python/lua/js) use it to reach the contract's author factory
    ///   and per-instance registry, since a single generic loader `create_instance`
    ///   has no other channel to the right contract. The runtime passes the
    ///   interface's `dispatch.vm.loader_data` for VM contracts and a null handle
    ///   for native contracts.
    /// - `host`: HostApi pointer (for memory allocation via host->alloc, and the
    ///   host pointer handed to the author factory)
    /// - `args`: Optional initialization arguments (contract-specific)
    /// - `out_instance`: out-param; the new instance handle is written here. A
    ///   null `data` denotes a stateless instance or construction failure.
    ///
    /// # Nullability
    /// REQUIRED — never null. Failure is signalled by writing a null
    /// *instance handle*, not by a null callback. `register_guest_contract`
    /// rejects interfaces whose `create_instance` bits are null.
    ///
    /// # Thread Safety
    /// May be called from any thread. Implementation must handle synchronization.
    pub create_instance: unsafe extern "C" fn(
        loader_data: VmLoaderData,
        host: *const HostApi,
        args: *const (),
        out_instance: *mut GuestContractInstance,
    ),
    /// Destroy an instance of this contract.
    ///
    /// MUST be called before hot-reload for all instances.
    /// Failure to destroy instances causes memory leaks.
    ///
    /// # Arguments
    /// - `loader_data`: the VM loader's per-(bundle,runtime) data handle (mirrors
    ///   `create_instance`). Native contracts ignore it; VM contracts use it to
    ///   reach the per-instance registry the handle was minted into. The runtime
    ///   passes the interface's `dispatch.vm.loader_data` for VM contracts and a
    ///   null handle for native contracts.
    /// - `host`: HostApi pointer
    /// - `instance`: Instance handle to destroy
    ///
    /// # Nullability
    /// REQUIRED — never null. Stateless contracts must supply a no-op
    /// function. `register_guest_contract` rejects interfaces whose
    /// `destroy_instance` bits are null.
    ///
    /// # Safety
    /// After calling destroy_instance, the instance handle is invalid.
    pub destroy_instance: unsafe extern "C" fn(
        loader_data: VmLoaderData,
        host: *const HostApi,
        instance: GuestContractInstance,
    ),
    /// Union of dispatch mechanisms — access based on dispatch_type.
    ///
    /// For Native dispatch: use `dispatch.native.functions[fn_id]`.
    /// For VM dispatch: use `dispatch.vm.call(loader_data, instance, fn_id, args, out, arena, out_err)`.
    pub dispatch: DispatchMechanisms,
}

#[cfg(test)]
mod tests {
    use super::GuestContractInterface;
    use crate::host::HostApi;
    use core::mem::{align_of, offset_of, size_of};

    #[test]
    fn layout_guest_contract_interface() {
        // GuestContractInterface layout:
        //   contract_id (GuestContractId/u64): 8 bytes @ offset 0
        //   contract_version (Version/3xu32): 12 bytes @ offset 8
        //   dispatch_type (DispatchType/u32): 4 bytes @ offset 20
        //   [padding 4 bytes for alignment]
        //   create_instance (fn ptr): 8 bytes @ offset 24
        //   destroy_instance (fn ptr): 8 bytes @ offset 32
        //   dispatch (union): 16 bytes @ offset 40
        // Total: 56 bytes
        assert_eq!(size_of::<GuestContractInterface>(), 56);
        assert_eq!(align_of::<GuestContractInterface>(), 8);
        assert_eq!(offset_of!(GuestContractInterface, contract_id), 0);
        assert_eq!(offset_of!(GuestContractInterface, contract_version), 8);
        assert_eq!(offset_of!(GuestContractInterface, dispatch_type), 20);
        assert_eq!(offset_of!(GuestContractInterface, create_instance), 24);
        assert_eq!(offset_of!(GuestContractInterface, destroy_instance), 32);
        assert_eq!(offset_of!(GuestContractInterface, dispatch), 40);
    }

    /// Verify HostApi is pointer-sized for FFI compatibility.
    #[test]
    fn guest_contract_interface_uses_host_api() {
        assert_eq!(size_of::<*const HostApi>(), 8);
    }
}