// spell-checker:disable
#![allow(unreachable_pub)]

use super::{
    _ctypes::CArgObject,
    PyCArray, PyCData, PyCPointer, PyCStructure, PyCUnion, StgInfo,
    base::{CArgValue, CDATA_BUFFER_METHODS, ParamFunc, StgInfoFlags},
    simple::PyCSimple,
};
use crate::{
    AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
    builtins::{PyBytes, PyDict, PyStr, PyTuple, PyType, PyTypeRef},
    class::StaticType,
    function::FuncArgs,
    protocol::{BufferDescriptor, PyBuffer, PyNumberMethods},
    types::{AsBuffer, AsNumber, Callable, Constructor, Initializer, Representable},
    vm::thread::with_current_vm,
};
use alloc::borrow::Cow;
use core::ffi::c_void;
use core::fmt::Debug;
use core::ptr::NonNull;
use num_traits::{Signed, ToPrimitive};
use rustpython_common::lock::PyRwLock;
#[cfg(windows)]
use rustpython_host_env::ctypes::ComMethodError;
use rustpython_host_env::ctypes::{
    CTypeLayout, CallError, CallOptions, CallRet, CallValue, FfiCif, FfiCodePtr, FfiType,
    RawMemoryView, RawMemoryViewError, StringAtError, call, ffi_pointer_type, ffi_type_from_code,
    ffi_void_type, has_pointer_width, offset_address, pointer_bytes, pointer_format, pointer_size,
    simple_type_is_pointer, write_pointer_to_buffer_at, write_prefix_limited,
};

// Internal function addresses for special ctypes functions
pub(super) const INTERNAL_CAST_ADDR: usize = 1;
pub(super) const INTERNAL_STRING_AT_ADDR: usize = 2;
pub(super) const INTERNAL_WSTRING_AT_ADDR: usize = 3;
pub(super) const INTERNAL_MEMORYVIEW_AT_ADDR: usize = 4;

// PyCFuncPtr - Function pointer implementation

/// Convert any object to a pointer value for c_void_p arguments
/// Follows ConvParam logic for pointer types
fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult<CArgValue> {
    // 0. CArgObject (from byref()) -> buffer address + offset
    if let Some(carg) = value.downcast_ref::<CArgObject>() {
        // Get buffer address from the underlying object
        let base_addr = if let Some(cdata) = carg.obj.downcast_ref::<PyCData>() {
            cdata.buffer.read().as_ptr() as usize
        } else {
            return Err(vm.new_type_error(format!(
                "byref() argument must be a ctypes instance, not '{}'",
                carg.obj.class().name()
            )));
        };
        let addr = (base_addr as isize + carg.offset) as usize;
        return Ok(CArgValue::pointer(addr));
    }

    // 1. None -> NULL
    if value.is(&vm.ctx.none) {
        return Ok(CArgValue::pointer(0));
    }

    // 2. PyCArray -> buffer address (PyCArrayType_paramfunc)
    if let Some(array) = value.downcast_ref::<PyCArray>() {
        let addr = array.0.buffer.read().as_ptr() as usize;
        return Ok(CArgValue::pointer(addr));
    }

    // 3. PyCPointer -> stored pointer value
    if let Some(ptr) = value.downcast_ref::<PyCPointer>() {
        return Ok(CArgValue::pointer(ptr.get_ptr_value()));
    }

    // 4. PyCStructure -> buffer address
    if let Some(struct_obj) = value.downcast_ref::<PyCStructure>() {
        let addr = struct_obj.0.buffer.read().as_ptr() as usize;
        return Ok(CArgValue::pointer(addr));
    }

    // 5. PyCSimple (c_void_p, c_char_p, etc.) -> value from buffer
    if let Some(simple) = value.downcast_ref::<PyCSimple>() {
        let buffer = simple.0.buffer.read();
        if has_pointer_width(&buffer) {
            let addr = rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer);
            return Ok(CArgValue::pointer(addr));
        }
    }

    // 6. bytes -> buffer address (PyBytes_AsString)
    if let Some(bytes) = value.downcast_ref::<crate::builtins::PyBytes>() {
        let addr = bytes.as_bytes().as_ptr() as usize;
        return Ok(CArgValue::pointer(addr));
    }

    // 7. Integer -> direct value (PyLong_AsVoidPtr behavior)
    if let Ok(int_val) = value.try_int(vm) {
        let bigint = int_val.as_bigint();
        // Negative values: use signed conversion (allows -1 as 0xFFFF...)
        if bigint.is_negative() {
            if let Some(signed_val) = bigint.to_isize() {
                return Ok(CArgValue::pointer(signed_val as usize));
            }
        } else if let Some(unsigned_val) = bigint.to_usize() {
            return Ok(CArgValue::pointer(unsigned_val));
        }
        // Value out of range - raise OverflowError
        return Err(vm.new_overflow_error("int too large to convert to pointer"));
    }

    // 8. Check _as_parameter_ attribute ( recursive ConvParam)
    if let Ok(as_param) = value.get_attr("_as_parameter_", vm) {
        return convert_to_pointer(&as_param, vm);
    }

    Err(vm.new_type_error(format!(
        "cannot convert '{}' to c_void_p",
        value.class().name()
    )))
}

/// ConvParam-like conversion for when argtypes is None
/// Returns an Argument with value and optional keep object
fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult<Argument> {
    // 1. CArgObject (from byref() or paramfunc) -> use stored value
    if let Some(carg) = value.downcast_ref::<CArgObject>() {
        return Ok(Argument {
            keep: carg.keep.clone(),
            value: carg.value.clone(),
        });
    }

    // 2. None -> NULL pointer
    if value.is(&vm.ctx.none) {
        return Ok(Argument {
            keep: None,
            value: CArgValue::pointer(0),
        });
    }

    // 3. ctypes objects -> use paramfunc
    if let Ok(carg) = super::base::call_paramfunc(value, vm) {
        return Ok(Argument {
            keep: carg.keep,
            value: carg.value,
        });
    }

    // 4. Python str -> wide string pointer (like PyUnicode_AsWideCharString)
    if let Some(s) = value.downcast_ref::<PyStr>() {
        let wide_bytes = rustpython_host_env::ctypes::utf16z_bytes(s.as_wtf8());
        let keep = vm.ctx.new_bytes(wide_bytes);
        let addr = keep.as_bytes().as_ptr() as usize;
        return Ok(Argument {
            keep: Some(keep.into()),
            value: CArgValue::pointer(addr),
        });
    }

    // 9. Python bytes -> null-terminated buffer pointer
    // Need to ensure null termination like c_char_p
    if let Some(bytes) = value.downcast_ref::<PyBytes>() {
        let buffer = rustpython_host_env::ctypes::null_terminated_bytes(bytes.as_bytes());
        let keep = vm.ctx.new_bytes(buffer);
        let addr = keep.as_bytes().as_ptr() as usize;
        return Ok(Argument {
            keep: Some(keep.into()),
            value: CArgValue::pointer(addr),
        });
    }

    // 10. Python int -> i32 (default integer type)
    if let Ok(int_val) = value.try_int(vm) {
        let val = int_val.as_bigint().to_i32().unwrap_or(0);
        return Ok(Argument {
            keep: None,
            value: CArgValue::Int(val),
        });
    }

    // 11. Python float -> f64
    if let Ok(float_val) = value.try_float(vm) {
        return Ok(Argument {
            keep: None,
            value: CArgValue::Double(float_val.to_f64()),
        });
    }

    // 12. Check _as_parameter_ attribute
    if let Ok(as_param) = value.get_attr("_as_parameter_", vm) {
        return conv_param(&as_param, vm);
    }

    Err(vm.new_type_error(format!(
        "Don't know how to convert parameter {}",
        value.class().name()
    )))
}

trait ArgumentType {
    /// Convert an argument for this type into a foreign-call value plus an
    /// optional owner keeping any referenced memory alive.
    fn convert_object(
        &self,
        value: PyObjectRef,
        vm: &VirtualMachine,
    ) -> PyResult<(CArgValue, Option<PyObjectRef>)>;
}

impl ArgumentType for PyTypeRef {
    fn convert_object(
        &self,
        value: PyObjectRef,
        vm: &VirtualMachine,
    ) -> PyResult<(CArgValue, Option<PyObjectRef>)> {
        // Validate the argument type up front (mirrors the pre-conversion
        // check): pointer-like ctypes types are always acceptable; a simple
        // type must carry a known _type_ code; anything else is unsupported.
        let type_code = if self.fast_issubclass(CArgObject::static_type())
            || self.fast_issubclass(PyCPointer::static_type())
            || self.fast_issubclass(PyCStructure::static_type())
            || self.fast_issubclass(PyCUnion::static_type())
        {
            None
        } else {
            // Use get_attr to traverse MRO (for subclasses like MyInt(c_int))
            let typ = self
                .as_object()
                .get_attr(vm.ctx.intern_str("_type_"), vm)
                .ok()
                .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?;
            let typ = typ
                .downcast_ref::<PyStr>()
                .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?
                .to_string();
            if ffi_type_from_code(&typ).is_none() {
                return Err(vm.new_type_error(format!("Unsupported argument type: {typ}")));
            }
            Some(typ)
        };

        // Call from_param first to convert the value
        // converter = PyTuple_GET_ITEM(argtypes, i);
        // v = PyObject_CallOneArg(converter, arg);
        let from_param = self
            .as_object()
            .get_attr(vm.ctx.intern_str("from_param"), vm)?;
        let converted = from_param.call((value,), vm)?;

        // Then pass the converted value to ConvParam logic
        // CArgObject (from from_param) -> use stored value and keepalive directly
        if let Some(carg) = converted.downcast_ref::<CArgObject>() {
            return Ok((carg.value.clone(), carg.keep.clone()));
        }

        // None -> NULL pointer
        if vm.is_none(&converted) {
            return Ok((CArgValue::pointer(0), None));
        }

        // For pointer types (POINTER(T)), we need to pass the pointer VALUE stored in buffer
        if self.fast_issubclass(PyCPointer::static_type()) {
            if let Some(pointer) = converted.downcast_ref::<PyCPointer>() {
                return Ok((CArgValue::pointer(pointer.get_ptr_value()), None));
            }
            return Ok((convert_to_pointer(&converted, vm)?, None));
        }

        // For structure/union types, pass the aggregate by value: snapshot the
        // instance bytes and build its call layout from the argtype. A byref()
        // result is a CArgObject and was already handled above (stays a pointer).
        if self.fast_issubclass(PyCStructure::static_type())
            || self.fast_issubclass(PyCUnion::static_type())
        {
            if let Some(cdata) = converted.downcast_ref::<PyCData>() {
                let bytes = cdata.buffer.read().to_vec();
                let layout = self.stg_info_opt().map_or_else(
                    || CTypeLayout::Opaque { size: bytes.len() },
                    |stg| super::base::type_layout(self, &stg, vm),
                );
                // Keep the converted instance alive through the call: the
                // snapshot may embed pointers into buffers its keep-alive set
                // owns, which must outlive the foreign call.
                return Ok((CArgValue::aggregate(layout, bytes), Some(converted.clone())));
            }
            return Ok((convert_to_pointer(&converted, vm)?, None));
        }

        // For pointer types (c_void_p, c_char_p, c_wchar_p), handle as pointer
        if matches!(type_code.as_deref(), Some("P" | "z" | "Z")) {
            return Ok((convert_to_pointer(&converted, vm)?, None));
        }

        // PyCSimple (already a ctypes instance from from_param)
        if let Ok(simple) = converted.downcast::<PyCSimple>() {
            let code = type_code
                .as_deref()
                .and_then(|s| s.chars().next())
                .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?;
            return Ok((simple.to_carg_value(code), None));
        }

        Err(vm.new_type_error("Unsupported argument type"))
    }
}

// PyCFuncPtrType - Metaclass for function pointer types
// PyCFuncPtrType_init

#[pyclass(name = "PyCFuncPtrType", base = PyType, module = "_ctypes")]
#[derive(Debug)]
#[repr(transparent)]
pub(super) struct PyCFuncPtrType(PyType);

impl Initializer for PyCFuncPtrType {
    type Args = FuncArgs;

    fn init(zelf: PyRef<Self>, _args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
        let obj: PyObjectRef = zelf.into();
        let new_type: PyTypeRef = obj
            .downcast()
            .map_err(|_| vm.new_type_error("expected type"))?;

        new_type.check_not_initialized(vm)?;

        let ptr_size = pointer_size();
        let mut stg_info = StgInfo::new(ptr_size, ptr_size);
        stg_info.format = Some("X{}".to_string());
        stg_info.length = 1;
        stg_info.flags |= StgInfoFlags::TYPEFLAG_ISPOINTER;
        stg_info.paramfunc = ParamFunc::Pointer; // CFuncPtr is passed as a pointer

        let _ = new_type.init_type_data(stg_info);
        Ok(())
    }
}

#[pyclass(flags(IMMUTABLETYPE), with(Initializer))]
impl PyCFuncPtrType {
    #[pygetset(name = "__pointer_type__")]
    fn pointer_type(zelf: PyTypeRef, vm: &VirtualMachine) -> PyResult {
        super::base::pointer_type_get(&zelf, vm)
    }

    #[pygetset(name = "__pointer_type__", setter)]
    fn set_pointer_type(zelf: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
        super::base::pointer_type_set(&zelf, value, vm)
    }
}

/// PyCFuncPtr - Function pointer instance
/// Saved in _base.buffer
#[pyclass(
    module = "_ctypes",
    name = "CFuncPtr",
    base = PyCData,
    metaclass = "PyCFuncPtrType"
)]
#[repr(C)]
pub(super) struct PyCFuncPtr {
    pub _base: PyCData,
    /// Thunk for callbacks (keeps thunk alive)
    pub thunk: PyRwLock<Option<PyRef<PyCThunk>>>,
    /// Original Python callable (for callbacks)
    pub callable: PyRwLock<Option<PyObjectRef>>,
    /// Converters cache
    pub converters: PyRwLock<Option<PyObjectRef>>,
    /// Instance-level argtypes override
    pub argtypes: PyRwLock<Option<PyObjectRef>>,
    /// Instance-level restype override
    pub restype: PyRwLock<Option<PyObjectRef>>,
    /// Checker function
    pub checker: PyRwLock<Option<PyObjectRef>>,
    /// Error checking function
    pub errcheck: PyRwLock<Option<PyObjectRef>>,
    /// COM method vtable index
    /// When set, the function reads the function pointer from the vtable at call time
    #[cfg(windows)]
    pub index: PyRwLock<Option<usize>>,
    /// COM method IID (interface ID) for error handling
    #[cfg(windows)]
    pub iid: PyRwLock<Option<PyObjectRef>>,
    /// Parameter flags for COM methods (direction: IN=1, OUT=2, IN|OUT=4)
    /// Each element is (direction, name, default) tuple
    pub paramflags: PyRwLock<Option<PyObjectRef>>,
}

impl Debug for PyCFuncPtr {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("PyCFuncPtr")
            .field("func_ptr", &self.get_func_ptr())
            .finish()
    }
}

/// Extract pointer value from a ctypes argument (c_void_p conversion)
fn extract_ptr_from_arg(arg: &PyObject, vm: &VirtualMachine) -> PyResult<usize> {
    // Try CArgObject first - extract the wrapped pointer value, applying offset
    if let Some(carg) = arg.downcast_ref::<super::_ctypes::CArgObject>() {
        if carg.offset != 0
            && let Some(cdata) = carg.obj.downcast_ref::<PyCData>()
        {
            let base = cdata.buffer.read().as_ptr() as usize;
            return Ok(offset_address(base, carg.offset));
        }
        return extract_ptr_from_arg(&carg.obj, vm);
    }
    // Try to get pointer value from various ctypes types
    if let Some(ptr) = arg.downcast_ref::<PyCPointer>() {
        return Ok(ptr.get_ptr_value());
    }
    if let Some(simple) = arg.downcast_ref::<PyCSimple>() {
        let buffer = simple.0.buffer.read();
        if buffer.first_chunk::<{ size_of::<usize>() }>().is_some() {
            return Ok(rustpython_host_env::ctypes::read_pointer_from_buffer(
                &buffer,
            ));
        }
    }
    if let Some(cdata) = arg.downcast_ref::<PyCData>() {
        // For arrays/structures, return address of buffer
        return Ok(cdata.buffer.read().as_ptr() as usize);
    }
    // PyStr: return internal buffer address
    if let Some(s) = arg.downcast_ref::<PyStr>() {
        return Ok(s.as_bytes().as_ptr() as usize);
    }
    // PyBytes: return internal buffer address
    if let Some(bytes) = arg.downcast_ref::<PyBytes>() {
        return Ok(bytes.as_bytes().as_ptr() as usize);
    }
    // Try as integer
    if let Ok(int_val) = arg.try_int(vm) {
        return Ok(int_val.as_bigint().to_usize().unwrap_or(0));
    }
    Err(vm.new_type_error(format!(
        "cannot convert '{}' to pointer",
        arg.class().name()
    )))
}

/// string_at implementation - read bytes from memory at ptr
fn string_at_impl(ptr: usize, size: isize, vm: &VirtualMachine) -> PyResult {
    match rustpython_host_env::ctypes::string_at(ptr, size) {
        Ok(bytes) => Ok(vm.ctx.new_bytes(bytes).into()),
        Err(StringAtError::NullPointer) => Err(vm.new_value_error("NULL pointer access")),
        Err(StringAtError::TooLong) => Err(vm.new_overflow_error("string too long")),
    }
}

/// wstring_at implementation - read wide string from memory at ptr
fn wstring_at_impl(ptr: usize, size: isize, vm: &VirtualMachine) -> PyResult {
    match rustpython_host_env::ctypes::wstring_at(ptr, size) {
        Ok(text) => Ok(vm.ctx.new_str(text).into()),
        Err(StringAtError::NullPointer) => Err(vm.new_value_error("NULL pointer access")),
        Err(StringAtError::TooLong) => Err(vm.new_overflow_error("string too long")),
    }
}

/// A buffer wrapping raw memory at a given pointer, for zero-copy memoryview.
#[pyclass(name = "_RawMemoryBuffer", module = "_ctypes")]
#[derive(Debug, PyPayload)]
pub(super) struct RawMemoryBuffer {
    memory: RawMemoryView,
}

static RAW_MEMORY_BUFFER_METHODS: crate::protocol::BufferMethods = crate::protocol::BufferMethods {
    obj_bytes: |buffer| {
        let raw = buffer.obj_as::<RawMemoryBuffer>();
        let slice = unsafe { raw.memory.bytes() };
        rustpython_common::borrow::BorrowedValue::Ref(slice)
    },
    obj_bytes_mut: |buffer| {
        let raw = buffer.obj_as::<RawMemoryBuffer>();
        let slice = unsafe { raw.memory.bytes_mut() };
        rustpython_common::borrow::BorrowedValueMut::RefMut(slice)
    },
    release: |_| {},
    retain: |_| {},
};

#[pyclass(with(AsBuffer))]
impl RawMemoryBuffer {}

impl AsBuffer for RawMemoryBuffer {
    fn as_buffer(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyBuffer> {
        Ok(PyBuffer::new(
            zelf.to_owned().into(),
            BufferDescriptor::simple(zelf.memory.size(), zelf.memory.readonly()),
            &RAW_MEMORY_BUFFER_METHODS,
        ))
    }
}

/// memoryview_at implementation - create a memoryview from memory at ptr
fn memoryview_at_impl(ptr: usize, size: isize, readonly: bool, vm: &VirtualMachine) -> PyResult {
    use crate::builtins::PyMemoryView;

    let memory = RawMemoryView::new(ptr, size, readonly).map_err(|err| match err {
        RawMemoryViewError::NullPointer => vm.new_value_error("NULL pointer access"),
        RawMemoryViewError::NegativeSize => vm.new_value_error("negative size"),
    })?;
    let raw_buf = RawMemoryBuffer { memory }.into_pyobject(vm);
    let mv = PyMemoryView::from_object(&raw_buf, vm)?;
    Ok(mv.into_pyobject(vm))
}

// cast_check_pointertype
fn cast_check_pointertype(ctype: &PyObject, vm: &VirtualMachine) -> bool {
    use super::pointer::PyCPointerType;

    // PyCPointerTypeObject_Check
    if ctype.class().fast_issubclass(PyCPointerType::static_type()) {
        return true;
    }

    // PyCFuncPtrTypeObject_Check - TODO

    // simple pointer types via StgInfo.proto (c_void_p, c_char_p, etc.)
    if let Ok(type_attr) = ctype.get_attr("_type_", vm)
        && let Some(s) = type_attr.downcast_ref::<PyStr>()
    {
        let c = s
            .to_str()
            .expect("_type_ is validated as ASCII at type creation");
        if c.len() == 1 && "sPzUZXO".contains(c) {
            return true;
        }
    }

    false
}

/// cast implementation
/// _ctypes.c cast()
pub(super) fn cast_impl(
    obj: PyObjectRef,
    src: PyObjectRef,
    ctype: PyObjectRef,
    vm: &VirtualMachine,
) -> PyResult {
    // 1. cast_check_pointertype
    if !cast_check_pointertype(&ctype, vm) {
        return Err(vm.new_type_error(format!(
            "cast() argument 2 must be a pointer type, not {}",
            ctype.class().name()
        )));
    }

    // 2. Extract pointer value - matches c_void_p_from_param_impl order
    let ptr_value: usize = if vm.is_none(&obj) {
        // None → NULL pointer
        0
    } else if let Ok(int_val) = obj.try_int(vm) {
        // int/long → direct pointer value
        int_val.as_bigint().to_usize().unwrap_or(0)
    } else if let Some(bytes) = obj.downcast_ref::<PyBytes>() {
        // bytes → buffer address (c_void_p_from_param: PyBytes_Check)
        bytes.as_bytes().as_ptr() as usize
    } else if let Some(s) = obj.downcast_ref::<PyStr>() {
        // unicode/str → buffer address (c_void_p_from_param: PyUnicode_Check)
        s.as_bytes().as_ptr() as usize
    } else if let Some(ptr) = obj.downcast_ref::<PyCPointer>() {
        // Pointer instance → contained pointer value
        ptr.get_ptr_value()
    } else if let Some(simple) = obj.downcast_ref::<PyCSimple>() {
        // Simple type (c_void_p, c_char_p, etc.) → value from buffer
        let buffer = simple.0.buffer.read();
        rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer)
    } else if let Some(cdata) = obj.downcast_ref::<PyCData>() {
        // Array, Structure, Union → buffer address (b_ptr)
        cdata.buffer.read().as_ptr() as usize
    } else {
        return Err(vm.new_type_error(format!(
            "cast() argument 1 must be a ctypes instance, not {}",
            obj.class().name()
        )));
    };

    // 3. Create result instance
    let result = ctype.call((), vm)?;

    // 4. _objects reference tracking
    // Share _objects dict between source and result, add id(src): src
    if src.class().fast_issubclass(PyCData::static_type()) {
        // Get the source's _objects, create dict if needed
        let shared_objects: PyObjectRef = if let Some(src_cdata) = src.downcast_ref::<PyCData>() {
            let mut src_objects = src_cdata.objects.write();
            if src_objects.is_none() {
                // Create new dict
                let dict = vm.ctx.new_dict();
                *src_objects = Some(dict.clone().into());
                dict.into()
            } else if let Some(obj) = src_objects.as_ref() {
                if obj.downcast_ref::<PyDict>().is_none() {
                    // Convert to dict (keep existing reference)
                    let dict = vm.ctx.new_dict();
                    let id_key: PyObjectRef = vm.ctx.new_int(obj.get_id() as i64).into();
                    let _ = dict.set_item(&*id_key, obj.clone(), vm);
                    *src_objects = Some(dict.clone().into());
                    dict.into()
                } else {
                    obj.clone()
                }
            } else {
                vm.ctx.new_dict().into()
            }
        } else {
            vm.ctx.new_dict().into()
        };

        // Add id(src): src to the shared dict
        if let Some(dict) = shared_objects.downcast_ref::<PyDict>() {
            let id_key: PyObjectRef = vm.ctx.new_int(src.get_id() as i64).into();
            let _ = dict.set_item(&*id_key, src, vm);
        }

        // Set result's _objects to the shared dict
        if let Some(result_cdata) = result.downcast_ref::<PyCData>() {
            *result_cdata.objects.write() = Some(shared_objects);
        }
    }

    // 5. Store pointer value
    if let Some(ptr) = result.downcast_ref::<PyCPointer>() {
        ptr.set_ptr_value(ptr_value);
    } else if let Some(cdata) = result.downcast_ref::<PyCData>() {
        let mut buffer = cdata.buffer.write();
        write_pointer_to_buffer_at(buffer.to_mut(), 0, pointer_size(), ptr_value);
    }

    Ok(result)
}

impl PyCFuncPtr {
    /// Get function pointer address from buffer
    fn get_func_ptr(&self) -> usize {
        let buffer = self._base.buffer.read();
        rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer)
    }

    /// Create buffer with function pointer address
    fn make_ptr_buffer(addr: usize) -> Vec<u8> {
        pointer_bytes(addr)
    }
}

impl Constructor for PyCFuncPtr {
    type Args = FuncArgs;

    fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
        // Handle different argument forms:
        // 1. Empty args: create uninitialized (NULL pointer)
        // 2. One integer argument: function address
        // 3. Tuple argument: (name, dll) form
        // 4. Callable: callback creation

        let ptr_size = pointer_size();

        if args.args.is_empty() {
            return Self {
                _base: PyCData::from_bytes(vec![0u8; ptr_size], None),
                thunk: PyRwLock::new(None),
                callable: PyRwLock::new(None),
                converters: PyRwLock::new(None),
                argtypes: PyRwLock::new(None),
                restype: PyRwLock::new(None),
                checker: PyRwLock::new(None),
                errcheck: PyRwLock::new(None),
                #[cfg(windows)]
                index: PyRwLock::new(None),
                #[cfg(windows)]
                iid: PyRwLock::new(None),
                paramflags: PyRwLock::new(None),
            }
            .into_ref_with_type(vm, cls)
            .map(Into::into);
        }

        let first_arg = &args.args[0];

        // Check for COM method form: (index, name, [paramflags], [iid])
        // First arg is integer (vtable index), second arg is string (method name)
        if args.args.len() >= 2
            && first_arg.try_int(vm).is_ok()
            && args.args[1].downcast_ref::<PyStr>().is_some()
        {
            #[cfg(windows)]
            let index = first_arg.try_int(vm)?.as_bigint().to_usize().unwrap_or(0);

            // args[3] is iid (GUID struct, optional)
            // Also check if args[2] is a GUID (has Data1 attribute) when args[3] is not present
            #[cfg(windows)]
            let iid = args.args.get(3).cloned().or_else(|| {
                args.args.get(2).and_then(|arg| {
                    // If it's a GUID struct (has Data1 attribute), use it as IID
                    if arg.get_attr("Data1", vm).is_ok() {
                        Some(arg.clone())
                    } else {
                        None
                    }
                })
            });

            // args[2] is paramflags (tuple or None)
            let paramflags = args.args.get(2).filter(|arg| !vm.is_none(arg)).cloned();

            return Self {
                _base: PyCData::from_bytes(vec![0u8; ptr_size], None),
                thunk: PyRwLock::new(None),
                callable: PyRwLock::new(None),
                converters: PyRwLock::new(None),
                argtypes: PyRwLock::new(None),
                restype: PyRwLock::new(None),
                checker: PyRwLock::new(None),
                errcheck: PyRwLock::new(None),
                #[cfg(windows)]
                index: PyRwLock::new(Some(index)),
                #[cfg(windows)]
                iid: PyRwLock::new(iid),
                paramflags: PyRwLock::new(paramflags),
            }
            .into_ref_with_type(vm, cls)
            .map(Into::into);
        }

        // Check if first argument is an integer (function address)
        if let Ok(addr) = first_arg.try_int(vm) {
            let ptr_val = addr.as_bigint().to_usize().unwrap_or(0);
            return Self {
                _base: PyCData::from_bytes(Self::make_ptr_buffer(ptr_val), None),
                thunk: PyRwLock::new(None),
                callable: PyRwLock::new(None),
                converters: PyRwLock::new(None),
                argtypes: PyRwLock::new(None),
                restype: PyRwLock::new(None),
                checker: PyRwLock::new(None),
                errcheck: PyRwLock::new(None),
                #[cfg(windows)]
                index: PyRwLock::new(None),
                #[cfg(windows)]
                iid: PyRwLock::new(None),
                paramflags: PyRwLock::new(None),
            }
            .into_ref_with_type(vm, cls)
            .map(Into::into);
        }

        // Check if first argument is a tuple (name, dll) form
        if let Some(tuple) = first_arg.downcast_ref::<PyTuple>() {
            let name = tuple
                .first()
                .ok_or_else(|| vm.new_type_error("Expected a tuple with at least 2 elements"))?
                .downcast_ref::<PyStr>()
                .ok_or_else(|| vm.new_type_error("Expected a string"))?
                .to_string();
            let dll = tuple
                .iter()
                .nth(1)
                .ok_or_else(|| vm.new_type_error("Expected a tuple with at least 2 elements"))?
                .clone();

            // Get library handle and load function
            let handle = dll.try_int(vm);
            let handle = match handle {
                Ok(handle) => handle.as_bigint().clone(),
                Err(_) => dll
                    .get_attr("_handle", vm)?
                    .try_int(vm)?
                    .as_bigint()
                    .clone(),
            };
            let terminated = format!("{name}\0");
            let ptr_val = match rustpython_host_env::ctypes::lookup_function_symbol_addr(
                handle
                    .to_usize()
                    .ok_or_else(|| vm.new_value_error("Invalid handle"))?,
                terminated.as_bytes(),
            ) {
                Ok(addr) => {
                    if addr == 0 {
                        return Err(vm.new_attribute_error(format!("function '{name}' not found")));
                    }
                    addr
                }
                Err(rustpython_host_env::ctypes::LookupSymbolError::LibraryNotFound) => {
                    return Err(vm.new_value_error("Library not found"));
                }
                Err(rustpython_host_env::ctypes::LookupSymbolError::LibraryClosed) => 0,
                Err(rustpython_host_env::ctypes::LookupSymbolError::Load(err)) => {
                    return Err(vm.new_attribute_error(err));
                }
            };

            return Self {
                _base: PyCData::from_bytes(Self::make_ptr_buffer(ptr_val), None),
                thunk: PyRwLock::new(None),
                callable: PyRwLock::new(None),
                converters: PyRwLock::new(None),
                argtypes: PyRwLock::new(None),
                restype: PyRwLock::new(None),
                checker: PyRwLock::new(None),
                errcheck: PyRwLock::new(None),
                #[cfg(windows)]
                index: PyRwLock::new(None),
                #[cfg(windows)]
                iid: PyRwLock::new(None),
                paramflags: PyRwLock::new(None),
            }
            .into_ref_with_type(vm, cls)
            .map(Into::into);
        }

        // Check if first argument is a Python callable (callback creation)
        if first_arg.is_callable() {
            // Get argument types and result type from the class
            let class_argtypes = cls.get_attr(vm.ctx.intern_str("_argtypes_"));
            let class_restype = cls.get_attr(vm.ctx.intern_str("_restype_"));
            let class_flags = cls
                .get_attr(vm.ctx.intern_str("_flags_"))
                .and_then(|f| f.try_to_value::<u32>(vm).ok())
                .unwrap_or(0);

            // Create the thunk (C-callable wrapper for the Python function)
            let thunk = PyCThunk::new(
                first_arg.clone(),
                class_argtypes.clone(),
                class_restype.clone(),
                class_flags,
                vm,
            )?;
            let code_ptr = thunk.code_ptr();
            let ptr_val = code_ptr.0 as usize;

            // Store the thunk as a Python object to keep it alive
            let thunk_ref: PyRef<PyCThunk> = thunk.into_ref(&vm.ctx);

            return Self {
                _base: PyCData::from_bytes(Self::make_ptr_buffer(ptr_val), None),
                thunk: PyRwLock::new(Some(thunk_ref)),
                callable: PyRwLock::new(Some(first_arg.clone())),
                converters: PyRwLock::new(None),
                argtypes: PyRwLock::new(class_argtypes),
                restype: PyRwLock::new(class_restype),
                checker: PyRwLock::new(None),
                errcheck: PyRwLock::new(None),
                #[cfg(windows)]
                index: PyRwLock::new(None),
                #[cfg(windows)]
                iid: PyRwLock::new(None),
                paramflags: PyRwLock::new(None),
            }
            .into_ref_with_type(vm, cls)
            .map(Into::into);
        }

        Err(vm.new_type_error("Expected an integer address or a tuple"))
    }

    fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
        unimplemented!("use slot_new")
    }
}

// PyCFuncPtr call helpers (similar to callproc.c flow)

/// Handle internal function addresses (PYFUNCTYPE special cases)
/// Returns Some(result) if handled, None if should continue with normal call
fn handle_internal_func(addr: usize, args: &FuncArgs, vm: &VirtualMachine) -> Option<PyResult> {
    if addr == INTERNAL_CAST_ADDR {
        let result: PyResult<(PyObjectRef, PyObjectRef, PyObjectRef)> = args.clone().bind(vm);
        return Some(result.and_then(|(obj, src, ctype)| cast_impl(obj, src, ctype, vm)));
    }

    if addr == INTERNAL_STRING_AT_ADDR {
        let result: PyResult<(PyObjectRef, Option<PyObjectRef>)> = args.clone().bind(vm);
        return Some(result.and_then(|(ptr_arg, size_arg)| {
            let ptr = extract_ptr_from_arg(&ptr_arg, vm)?;
            let size = size_arg
                .and_then(|s| s.try_int(vm).ok())
                .and_then(|i| i.as_bigint().to_isize())
                .unwrap_or(-1);
            string_at_impl(ptr, size, vm)
        }));
    }

    if addr == INTERNAL_WSTRING_AT_ADDR {
        let result: PyResult<(PyObjectRef, Option<PyObjectRef>)> = args.clone().bind(vm);
        return Some(result.and_then(|(ptr_arg, size_arg)| {
            let ptr = extract_ptr_from_arg(&ptr_arg, vm)?;
            let size = size_arg
                .and_then(|s| s.try_int(vm).ok())
                .and_then(|i| i.as_bigint().to_isize())
                .unwrap_or(-1);
            wstring_at_impl(ptr, size, vm)
        }));
    }

    if addr == INTERNAL_MEMORYVIEW_AT_ADDR {
        let result: PyResult<(PyObjectRef, PyObjectRef, Option<PyObjectRef>)> =
            args.clone().bind(vm);
        return Some(result.and_then(|(ptr_arg, size_arg, readonly_arg)| {
            let ptr = extract_ptr_from_arg(&ptr_arg, vm)?;
            let size_int = size_arg.try_int(vm)?;
            let size = size_int
                .as_bigint()
                .to_isize()
                .ok_or_else(|| vm.new_value_error("size too large"))?;
            let readonly = readonly_arg
                .and_then(|r| r.try_int(vm).ok())
                .and_then(|i| i.as_bigint().to_i32())
                .unwrap_or(0)
                != 0;
            memoryview_at_impl(ptr, size, readonly, vm)
        }));
    }

    None
}

/// How the foreign call's return value is retrieved (mirrors `CallRet`).
enum RetSpec {
    /// restype is None: void return.
    Void,
    /// Pointer-valued return (a `TYPEFLAG_ISPOINTER` restype, or an oversized
    /// by-value struct approximated as a pointer-sized register).
    Pointer,
    /// A scalar retrieved as the given ctypes simple-type code.
    Code(char),
    /// A by-value aggregate (struct/union) return with the given call layout.
    Aggregate(CTypeLayout),
}

/// Call information extracted from PyCFuncPtr (argtypes, restype, etc.)
struct CallInfo {
    explicit_arg_types: Option<Vec<PyTypeRef>>,
    restype_obj: Option<PyObjectRef>,
    ret: RetSpec,
}

/// Determine how to retrieve the return value from restype, reproducing the
/// prior `ffi_return_type` + `is_pointer_return` dispatch.
fn compute_ret_spec(
    restype_is_none: bool,
    restype_obj: Option<&PyObjectRef>,
    vm: &VirtualMachine,
) -> RetSpec {
    if restype_is_none {
        return RetSpec::Void;
    }
    let Some(restype_type) = restype_obj.and_then(|t| t.clone().downcast::<PyType>().ok()) else {
        return RetSpec::Code('i');
    };

    // Pointer return via TYPEFLAG_ISPOINTER (c_void_p, c_char_p, c_wchar_p, POINTER(T))
    if restype_type
        .stg_info_opt()
        .is_some_and(|info| info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER))
    {
        return RetSpec::Pointer;
    }

    // Simple type via its _type_ code (traversing MRO)
    if let Ok(type_attr) = restype_type
        .as_object()
        .get_attr(vm.ctx.intern_str("_type_"), vm)
        && let Some(s) = type_attr.downcast_ref::<PyStr>()
        && let Some(code) = s.to_str()
        && ffi_type_from_code(code).is_some()
    {
        return if simple_type_is_pointer(code) {
            RetSpec::Pointer
        } else {
            RetSpec::Code(code.chars().next().unwrap_or('i'))
        };
    }

    // Structure/Union (StgInfo, no _type_): returned by value as an aggregate.
    // The layout is built from the held guard to avoid re-locking the type.
    if let Some(stg_info) = restype_type.stg_info_opt() {
        return match stg_info.paramfunc {
            ParamFunc::Structure | ParamFunc::Union => {
                RetSpec::Aggregate(super::base::type_layout(&restype_type, &stg_info, vm))
            }
            // Any other aggregate-ish StgInfo without a code: size-approximated
            // register return, as before.
            _ => {
                let size = stg_info.size;
                if size <= 4 {
                    RetSpec::Code('i')
                } else if size <= 8 {
                    RetSpec::Code('q')
                } else {
                    RetSpec::Pointer
                }
            }
        };
    }

    RetSpec::Code('i')
}

/// Extract call information (argtypes, restype) from PyCFuncPtr
fn extract_call_info(zelf: &Py<PyCFuncPtr>, vm: &VirtualMachine) -> PyResult<CallInfo> {
    // Get argtypes - first from instance, then from type's _argtypes_
    let explicit_arg_types: Option<Vec<PyTypeRef>> =
        if let Some(argtypes_obj) = zelf.argtypes.read().as_ref() {
            if !vm.is_none(argtypes_obj) {
                Some(
                    argtypes_obj
                        .try_to_value::<Vec<PyObjectRef>>(vm)?
                        .into_iter()
                        .filter_map(|obj| obj.downcast::<PyType>().ok())
                        .collect(),
                )
            } else {
                None // argtypes is None -> use ConvParam
            }
        } else if let Some(class_argtypes) = zelf
            .as_object()
            .class()
            .get_attr(vm.ctx.intern_str("_argtypes_"))
            && !vm.is_none(&class_argtypes)
        {
            Some(
                class_argtypes
                    .try_to_value::<Vec<PyObjectRef>>(vm)?
                    .into_iter()
                    .filter_map(|obj| obj.downcast::<PyType>().ok())
                    .collect(),
            )
        } else {
            None // No argtypes -> use ConvParam
        };

    // Get restype - first from instance, then from class's _restype_
    let restype_obj = zelf.restype.read().clone().or_else(|| {
        zelf.as_object()
            .class()
            .get_attr(vm.ctx.intern_str("_restype_"))
    });

    // Check if restype is explicitly None (return void)
    let restype_is_none = restype_obj.as_ref().is_some_and(|t| vm.is_none(t));
    let ret = compute_ret_spec(restype_is_none, restype_obj.as_ref(), vm);

    Ok(CallInfo {
        explicit_arg_types,
        restype_obj,
        ret,
    })
}

/// Parsed paramflags: (direction, name, default) tuples
/// direction: 1=IN, 2=OUT, 4=IN|OUT (or 1|2=3)
type ParsedParamFlags = Vec<(u32, Option<String>, Option<PyObjectRef>)>;

/// Parse paramflags from PyCFuncPtr
fn parse_paramflags(
    zelf: &Py<PyCFuncPtr>,
    vm: &VirtualMachine,
) -> PyResult<Option<ParsedParamFlags>> {
    let Some(pf) = zelf.paramflags.read().as_ref().cloned() else {
        return Ok(None);
    };

    let pf_vec = pf.try_to_value::<Vec<PyObjectRef>>(vm)?;
    let parsed = pf_vec
        .into_iter()
        .map(|item| {
            let Some(tuple) = item.downcast_ref::<PyTuple>() else {
                // Single value means just the direction
                let direction = item
                    .try_int(vm)
                    .ok()
                    .and_then(|i| i.as_bigint().to_u32())
                    .unwrap_or(1);
                return (direction, None, None);
            };
            let direction = tuple
                .first()
                .and_then(|d| d.try_int(vm).ok())
                .and_then(|i| i.as_bigint().to_u32())
                .unwrap_or(1);
            let name = tuple
                .get(1)
                .and_then(|n| n.downcast_ref::<PyStr>().map(|s| s.to_string()));
            let default = tuple.get(2).cloned();
            (direction, name, default)
        })
        .collect();
    Ok(Some(parsed))
}

/// Resolve COM method pointer from vtable (Windows only)
/// Returns (Some(CodePtr), true) if this is a COM method call, (None, false) otherwise
#[cfg(windows)]
fn resolve_com_method(
    zelf: &Py<PyCFuncPtr>,
    args: &FuncArgs,
    vm: &VirtualMachine,
) -> PyResult<(Option<FfiCodePtr>, bool)> {
    let com_index = zelf.index.read();
    let Some(idx) = *com_index else {
        return Ok((None, false));
    };

    // First arg must be the COM object pointer
    if args.args.is_empty() {
        return Err(vm.new_type_error("COM method requires at least one argument (self)"));
    }

    // Extract COM pointer value from first argument
    let self_arg = &args.args[0];
    let com_ptr = if let Some(simple) = self_arg.downcast_ref::<PyCSimple>() {
        let buffer = simple.0.buffer.read();
        if has_pointer_width(&buffer) {
            rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer)
        } else {
            0
        }
    } else if let Ok(int_val) = self_arg.try_int(vm) {
        int_val.as_bigint().to_usize().unwrap_or(0)
    } else {
        return Err(vm.new_type_error("COM method first argument must be a COM pointer"));
    };

    let code_ptr = match rustpython_host_env::ctypes::resolve_com_vtable_entry(com_ptr, idx) {
        Ok(code_ptr) => code_ptr,
        Err(ComMethodError::NullComPointer) => {
            return Err(vm.new_value_error("NULL COM pointer access"));
        }
        Err(ComMethodError::NullVtablePointer) => {
            return Err(vm.new_value_error("NULL vtable pointer"));
        }
        Err(ComMethodError::NullFunctionPointer) => {
            return Err(vm.new_value_error("NULL function pointer in vtable"));
        }
    };

    Ok((Some(code_ptr), true))
}

/// Single argument for FFI call
// struct argument
struct Argument {
    value: CArgValue,
    #[allow(dead_code)]
    keep: Option<PyObjectRef>, // Object to keep alive during call
}

/// Out buffers for paramflags OUT parameters
type OutBuffers = Vec<(usize, PyObjectRef)>;

/// Get buffer address from a ctypes object
fn get_buffer_addr(obj: &PyObjectRef) -> Option<usize> {
    obj.downcast_ref::<PyCSimple>()
        .map(|s| s.0.buffer.read().as_ptr() as usize)
        .or_else(|| {
            obj.downcast_ref::<super::structure::PyCStructure>()
                .map(|s| s.0.buffer.read().as_ptr() as usize)
        })
        .or_else(|| {
            obj.downcast_ref::<PyCPointer>()
                .map(|s| s.0.buffer.read().as_ptr() as usize)
        })
}

/// Create OUT buffer for a parameter type
fn create_out_buffer(arg_type: &PyTypeRef, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
    // For POINTER(T) types, create T instance (the pointed-to type)
    if arg_type.fast_issubclass(PyCPointer::static_type())
        && let Some(stg_info) = arg_type.stg_info_opt()
        && let Some(ref proto) = stg_info.proto
    {
        return proto.as_object().call((), vm);
    }
    // Not a pointer type or no proto, create instance directly
    arg_type.as_object().call((), vm)
}

/// Build callargs when no argtypes specified (use ConvParam)
fn build_callargs_no_argtypes(
    args: &FuncArgs,
    vm: &VirtualMachine,
) -> PyResult<(Vec<Argument>, OutBuffers)> {
    let arguments: Vec<Argument> = args
        .args
        .iter()
        .map(|arg| conv_param(arg, vm))
        .collect::<PyResult<Vec<_>>>()?;
    Ok((arguments, Vec::new()))
}

/// Build callargs for regular function with argtypes (no paramflags)
fn build_callargs_simple(
    args: &FuncArgs,
    arg_types: &[PyTypeRef],
    vm: &VirtualMachine,
) -> PyResult<(Vec<Argument>, OutBuffers)> {
    let arguments: Vec<Argument> = args
        .args
        .iter()
        .enumerate()
        .map(|(n, arg)| {
            let arg_type = arg_types
                .get(n)
                .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?;
            let (value, keep) = arg_type.convert_object(arg.clone(), vm)?;
            Ok(Argument { value, keep })
        })
        .collect::<PyResult<Vec<_>>>()?;
    Ok((arguments, Vec::new()))
}

/// Build callargs with paramflags (handles IN/OUT parameters)
fn build_callargs_with_paramflags(
    args: &FuncArgs,
    arg_types: &[PyTypeRef],
    paramflags: &ParsedParamFlags,
    skip_first_arg: bool, // true for COM methods
    vm: &VirtualMachine,
) -> PyResult<(Vec<Argument>, OutBuffers)> {
    let mut arguments = Vec::new();
    let mut out_buffers = Vec::new();

    // For COM methods, first arg is self (pointer)
    let mut caller_arg_idx = if skip_first_arg {
        if !args.args.is_empty() {
            let arg = conv_param(&args.args[0], vm)?;
            arguments.push(arg);
        }
        1usize
    } else {
        0usize
    };

    // Process parameters based on paramflags
    for (param_idx, (direction, _name, default)) in paramflags.iter().enumerate() {
        let arg_type = arg_types
            .get(param_idx)
            .ok_or_else(|| vm.new_type_error("paramflags/argtypes mismatch"))?;

        let is_out = (*direction & 2) != 0; // OUT flag
        let is_in = (*direction & 1) != 0 || *direction == 0; // IN flag or default

        if is_out && !is_in {
            // Pure OUT parameter: create buffer, don't consume caller arg
            let buffer = create_out_buffer(arg_type, vm)?;
            let addr = get_buffer_addr(&buffer)
                .ok_or_else(|| vm.new_type_error("Cannot create OUT buffer for this type"))?;
            arguments.push(Argument {
                keep: None,
                value: CArgValue::pointer(addr),
            });
            out_buffers.push((param_idx, buffer));
        } else {
            // IN or IN|OUT: get from caller args or default
            let arg = if caller_arg_idx < args.args.len() {
                caller_arg_idx += 1;
                args.args[caller_arg_idx - 1].clone()
            } else if let Some(def) = default {
                def.clone()
            } else {
                return Err(vm.new_type_error(format!("required argument {param_idx} missing")));
            };

            if is_out {
                // IN|OUT: track for return
                out_buffers.push((param_idx, arg.clone()));
            }
            let (value, keep) = arg_type.convert_object(arg, vm)?;
            arguments.push(Argument { value, keep });
        }
    }

    Ok((arguments, out_buffers))
}

/// Build call arguments (main dispatcher)
fn build_callargs(
    args: &FuncArgs,
    call_info: &CallInfo,
    paramflags: Option<&ParsedParamFlags>,
    is_com_method: bool,
    vm: &VirtualMachine,
) -> PyResult<(Vec<Argument>, OutBuffers)> {
    let Some(ref arg_types) = call_info.explicit_arg_types else {
        // No argtypes: use ConvParam
        return build_callargs_no_argtypes(args, vm);
    };

    if let Some(pflags) = paramflags {
        // Has paramflags: handle IN/OUT
        build_callargs_with_paramflags(args, arg_types, pflags, is_com_method, vm)
    } else if is_com_method {
        // COM method without paramflags
        let mut arguments = Vec::new();
        if !args.args.is_empty() {
            arguments.push(conv_param(&args.args[0], vm)?);
        }
        for (n, arg) in args.args.iter().skip(1).enumerate() {
            let arg_type = arg_types
                .get(n)
                .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?;
            let (value, keep) = arg_type.convert_object(arg.clone(), vm)?;
            arguments.push(Argument { value, keep });
        }
        Ok((arguments, Vec::new()))
    } else {
        // Regular function
        build_callargs_simple(args, arg_types, vm)
    }
}

/// Execute the foreign call through the unified `call` entry point.
fn ctypes_callproc(
    addr: usize,
    arguments: &[Argument],
    ret: &RetSpec,
    options: CallOptions,
) -> Result<CallValue, CallError> {
    // Encode each simple-type code into its own buffer so the `&str` borrowed
    // by `CallArg::Typed` outlives the call.
    let mut code_bufs = vec![[0u8; 4]; arguments.len()];
    let call_args: Vec<_> = arguments
        .iter()
        .zip(code_bufs.iter_mut())
        .map(|(arg, code_buf)| arg.value.as_call_arg(code_buf))
        .collect();

    let mut ret_code_buf = [0u8; 4];
    let call_ret = match ret {
        RetSpec::Void => CallRet::Void,
        RetSpec::Pointer => CallRet::Pointer,
        RetSpec::Code(code) => CallRet::Code(code.encode_utf8(&mut ret_code_buf)),
        RetSpec::Aggregate(layout) => CallRet::Aggregate(layout),
    };

    call(addr, &call_args, call_ret, options)
}

/// Check and handle HRESULT errors (Windows)
#[cfg(windows)]
fn check_hresult(hresult: i32, zelf: &Py<PyCFuncPtr>, vm: &VirtualMachine) -> PyResult<()> {
    if hresult >= 0 {
        return Ok(());
    }

    if zelf.iid.read().is_some() {
        // Raise COMError
        let ctypes_module = vm.import("_ctypes", 0)?;
        let com_error_type = ctypes_module.get_attr("COMError", vm)?;
        let com_error_type = com_error_type
            .downcast::<PyType>()
            .map_err(|_| vm.new_type_error("COMError is not a type"))?;
        let hresult_obj: PyObjectRef = vm.ctx.new_int(hresult).into();
        let text: PyObjectRef = vm
            .ctx
            .new_str(format!("HRESULT: 0x{:08X}", hresult as u32))
            .into();
        let details: PyObjectRef = vm.ctx.none();
        let exc = vm.invoke_exception(&com_error_type, vec![text.clone(), details.clone()])?;
        let _ = exc.as_object().set_attr("hresult", hresult_obj, vm);
        let _ = exc.as_object().set_attr("text", text, vm);
        let _ = exc.as_object().set_attr("details", details, vm);
        Err(exc)
    } else {
        // Raise OSError
        let exc = vm.new_os_error(format!("HRESULT: 0x{:08X}", hresult as u32));
        let _ = exc
            .as_object()
            .set_attr("winerror", vm.ctx.new_int(hresult), vm);
        Err(exc)
    }
}

/// Convert the foreign-call result to a Python object
// = GetResult
fn convert_raw_result(
    result: &CallValue,
    call_info: &CallInfo,
    vm: &VirtualMachine,
) -> Option<PyObjectRef> {
    // Result register image as bytes + size (None for void): pointer/scalar
    // returns are pointer/register sized.
    let (result_bytes, result_size) = match result {
        CallValue::Void => return None,
        CallValue::Pointer(ptr) => (ptr.to_ne_bytes().to_vec(), size_of::<usize>()),
        CallValue::Scalar(bytes) | CallValue::Aggregate(bytes) => (bytes.clone(), bytes.len()),
    };

    // Integer view of the return register, for the fallback branches below.
    let result_word: usize = match result {
        CallValue::Pointer(ptr) => *ptr,
        CallValue::Scalar(bytes) | CallValue::Aggregate(bytes) => {
            let mut word = [0u8; size_of::<usize>()];
            let n = bytes.len().min(word.len());
            word[..n].copy_from_slice(&bytes[..n]);
            usize::from_ne_bytes(word)
        }
        CallValue::Void => 0,
    };

    // 1. No restype → return as int
    let restype = match &call_info.restype_obj {
        None => {
            // Default: return as int
            return Some(vm.ctx.new_int(result_word as isize).into());
        }
        Some(r) => r,
    };

    // 2. restype is None → return None
    if restype.is(&vm.ctx.none()) {
        return None;
    }

    // 3. Get restype as PyType
    let restype_type = match restype.clone().downcast::<PyType>() {
        Ok(t) => t,
        Err(_) => {
            // Not a type, call it with int result
            return restype.call((result_word as isize,), vm).ok();
        }
    };

    // 4. Get StgInfo
    let stg_info = restype_type.stg_info_opt();

    // No StgInfo → call restype with int
    if stg_info.is_none() {
        return restype_type
            .as_object()
            .call((result_word as isize,), vm)
            .ok();
    }

    let info = stg_info.unwrap();
    // Extract what's needed and release the read guard before constructing any
    // instance below: instance construction write-locks the type's StgInfo (to
    // finalize it), which would self-deadlock against a held read guard.
    let is_pointer_type = info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER);
    let has_proto = info.proto.is_some();
    drop(info);

    // py_object: interpret return value as PyObject* and materialize it.
    if let Ok(type_attr) = restype_type
        .as_object()
        .get_attr(vm.ctx.intern_str("_type_"), vm)
        && let Some(type_str) = type_attr.downcast_ref::<PyStr>()
        && type_str.to_str() == Some("O")
    {
        let ptr = NonNull::new(result_word as *mut PyObject).or_else(|| {
            vm.set_exception(Some(vm.new_value_error("PyObject is NULL")));
            None
        })?;
        unsafe {
            let obj = PyObjectRef::from_raw(ptr);
            return Some(obj);
        }
    }

    // 5. Simple type with getfunc → use bytes_to_pyobject (info->getfunc)
    // is_simple_instance returns TRUE for c_int, c_void_p, etc.
    if super::base::is_simple_instance(&restype_type) {
        return Some(super::base::bytes_to_pyobject(
            &restype_type,
            &result_bytes,
            vm,
        ));
    }

    // 6. Complex type → create ctypes instance (PyCData_FromBaseObj)
    // This handles POINTER(T), Structure, Array, etc.

    // Special handling for POINTER(T) types - set pointer value directly
    if is_pointer_type
        && has_proto
        && let CallValue::Pointer(ptr) = result
        && let Ok(instance) = restype_type.as_object().call((), vm)
    {
        if let Some(pointer) = instance.downcast_ref::<PyCPointer>() {
            pointer.set_ptr_value(*ptr);
        }
        return Some(instance);
    }

    // Create instance and copy result data
    pycdata_from_ffi_result(&restype_type, &result_bytes, result_size, vm).ok()
}

/// Create a ctypes instance from FFI result (PyCData_FromBaseObj equivalent)
fn pycdata_from_ffi_result(
    typ: &PyTypeRef,
    result_bytes: &[u8],
    size: usize,
    vm: &VirtualMachine,
) -> PyResult {
    // Create instance
    let instance = PyType::call(typ, ().into(), vm)?;

    // Copy result data into instance buffer
    if let Some(cdata) = instance.downcast_ref::<PyCData>() {
        let mut buffer = cdata.buffer.write();
        write_prefix_limited(buffer.to_mut(), result_bytes, size);
    }

    Ok(instance)
}

/// Extract values from OUT buffers
fn extract_out_values(
    out_buffers: Vec<(usize, PyObjectRef)>,
    vm: &VirtualMachine,
) -> Vec<PyObjectRef> {
    out_buffers
        .into_iter()
        .map(|(_, buffer)| buffer.get_attr("value", vm).unwrap_or(buffer))
        .collect()
}

/// Build final result (main function)
fn build_result(
    call_result: CallValue,
    call_info: &CallInfo,
    out_buffers: OutBuffers,
    zelf: &Py<PyCFuncPtr>,
    args: &FuncArgs,
    vm: &VirtualMachine,
) -> PyResult {
    // Check HRESULT on Windows
    #[cfg(windows)]
    if let CallValue::Scalar(bytes) = &call_result {
        let is_hresult = call_info
            .restype_obj
            .as_ref()
            .and_then(|t| t.clone().downcast::<PyType>().ok())
            .is_some_and(|t| t.name().to_string() == "HRESULT");
        if is_hresult {
            let mut word = [0u8; size_of::<usize>()];
            let n = bytes.len().min(word.len());
            word[..n].copy_from_slice(&bytes[..n]);
            check_hresult(usize::from_ne_bytes(word) as i32, zelf, vm)?;
        }
    }

    // Convert the foreign-call result to a Python object
    let mut result = convert_raw_result(&call_result, call_info, vm);

    // Apply errcheck if set
    if let Some(errcheck) = zelf.errcheck.read().as_ref() {
        let args_tuple = PyTuple::new_ref(args.args.clone(), &vm.ctx);
        let func_obj = zelf.as_object().to_owned();
        let result_obj = result.clone().unwrap_or_else(|| vm.ctx.none());
        let checked = errcheck.call((result_obj, func_obj, args_tuple.clone()), vm)?;
        // Returning the original args tuple requests normal result processing.
        if !checked.is(&args_tuple) {
            result = Some(checked);
        }
    }

    // Handle OUT parameter return values
    if out_buffers.is_empty() {
        return result.map_or_else(|| Ok(vm.ctx.none()), Ok);
    }

    let out_values = extract_out_values(out_buffers, vm);
    Ok(match <[PyObjectRef; 1]>::try_from(out_values) {
        Ok([single]) => single,
        Err(v) => PyTuple::new_ref(v, &vm.ctx).into(),
    })
}

impl Callable for PyCFuncPtr {
    type Args = FuncArgs;
    fn call(zelf: &Py<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult {
        // 1. Check for internal PYFUNCTYPE addresses
        if let Some(result) = handle_internal_func(zelf.get_func_ptr(), &args, vm) {
            return result;
        }

        // 2. Resolve function pointer (COM or direct)
        #[cfg(windows)]
        let (func_ptr, is_com_method) = resolve_com_method(zelf, &args, vm)?;
        #[cfg(not(windows))]
        let (func_ptr, is_com_method) = (None::<FfiCodePtr>, false);

        // 3. Extract call info (argtypes, restype)
        let call_info = extract_call_info(zelf, vm)?;

        // 4. Parse paramflags
        let paramflags = parse_paramflags(zelf, vm)?;

        // 5. Build call arguments
        let (arguments, out_buffers) =
            build_callargs(&args, &call_info, paramflags.as_ref(), is_com_method, vm)?;

        // 6. Function address (usize); the unified `call` rejects a NULL address.
        let addr = match func_ptr {
            Some(cp) => cp.0 as usize,
            None => zelf.get_func_ptr(),
        };

        // 7. Errno / last-error swap options from flags
        let flags = Self::_flags_(zelf, vm);
        let options = CallOptions {
            use_errno: flags & super::base::StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0,
            use_last_error: flags & super::base::StgInfoFlags::FUNCFLAG_USE_LASTERROR.bits() != 0,
        };

        // 8. Call the function through the unified entry point.
        let call_result = ctypes_callproc(addr, &arguments, &call_info.ret, options).map_err(
            |err| match err {
                CallError::NullFunctionPointer => vm.new_value_error("NULL function pointer"),
                CallError::UnknownTypeCode(code) => {
                    vm.new_type_error(format!("Unsupported argument type: {code}"))
                }
                CallError::BufferTooSmall { expected, got } => vm.new_value_error(format!(
                    "argument buffer too small: expected {expected}, got {got}"
                )),
            },
        )?;

        // 9. Build result
        build_result(call_result, &call_info, out_buffers, zelf, &args, vm)
    }
}

// PyCFuncPtr_repr
impl Representable for PyCFuncPtr {
    fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
        let type_name = zelf.class().name();
        // Use object id, not function pointer address
        let addr = zelf.get_id();
        Ok(format!("<{type_name} object at {addr:#x}>"))
    }
}

// PyCData_NewGetBuffer
impl AsBuffer for PyCFuncPtr {
    fn as_buffer(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyBuffer> {
        // CFuncPtr types may not have StgInfo if PyCFuncPtrType metaclass is not used
        // Use default values for function pointers: format="X{}", size=sizeof(pointer)
        let (format, itemsize) = if let Some(stg_info) = zelf.class().stg_info_opt() {
            (
                stg_info
                    .format
                    .clone()
                    .map_or(Cow::Borrowed("X{}"), Cow::Owned),
                stg_info.size,
            )
        } else {
            (Cow::Borrowed(pointer_format()), pointer_size())
        };
        let desc = BufferDescriptor {
            len: itemsize,
            readonly: false,
            itemsize,
            format,
            dim_desc: vec![],
        };
        let buf = PyBuffer::new(zelf.to_owned().into(), desc, &CDATA_BUFFER_METHODS);
        Ok(buf)
    }
}

#[pyclass(
    flags(BASETYPE),
    with(Callable, Constructor, AsNumber, Representable, AsBuffer)
)]
impl PyCFuncPtr {
    // restype getter/setter
    #[pygetset]
    fn restype(&self) -> Option<PyObjectRef> {
        self.restype.read().clone()
    }

    #[pygetset(setter)]
    fn set_restype(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
        // Must be type, callable, or None
        if vm.is_none(&value) {
            *self.restype.write() = None;
        } else if value.downcast_ref::<PyType>().is_some() || value.is_callable() {
            *self.restype.write() = Some(value);
        } else {
            return Err(vm.new_type_error("restype must be a type, a callable, or None"));
        }
        Ok(())
    }

    // argtypes getter/setter
    #[pygetset]
    fn argtypes(&self, vm: &VirtualMachine) -> PyObjectRef {
        self.argtypes
            .read()
            .clone()
            .unwrap_or_else(|| vm.ctx.empty_tuple.clone().into())
    }

    #[pygetset(name = "argtypes", setter)]
    fn set_argtypes(&self, value: PyObjectRef, vm: &VirtualMachine) {
        if vm.is_none(&value) {
            *self.argtypes.write() = None;
        } else {
            // Store the argtypes object directly as it is
            *self.argtypes.write() = Some(value);
        }
    }

    // errcheck getter/setter
    #[pygetset]
    fn errcheck(&self) -> Option<PyObjectRef> {
        self.errcheck.read().clone()
    }

    #[pygetset(setter)]
    fn set_errcheck(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
        if vm.is_none(&value) {
            *self.errcheck.write() = None;
        } else if value.is_callable() {
            *self.errcheck.write() = Some(value);
        } else {
            return Err(vm.new_type_error("errcheck must be a callable or None"));
        }
        Ok(())
    }

    // _flags_ getter (read-only, from type's class attribute or StgInfo)
    #[pygetset]
    fn _flags_(zelf: &Py<Self>, vm: &VirtualMachine) -> u32 {
        // First try to get _flags_ from type's class attribute (for dynamically created types)
        // This is how CDLL sets use_errno: class _FuncPtr(_CFuncPtr): _flags_ = flags
        if let Ok(flags_attr) = zelf.class().as_object().get_attr("_flags_", vm)
            && let Ok(flags_int) = flags_attr.try_to_value::<u32>(vm)
        {
            return flags_int;
        }

        // Fallback to StgInfo for native types
        zelf.class()
            .stg_info_opt()
            .map_or(StgInfoFlags::empty().bits(), |stg| stg.flags.bits())
    }
}

impl AsNumber for PyCFuncPtr {
    fn as_number() -> &'static PyNumberMethods {
        static AS_NUMBER: PyNumberMethods = PyNumberMethods {
            boolean: Some(|number, _vm| {
                let zelf = number.obj.downcast_ref::<PyCFuncPtr>().unwrap();
                Ok(zelf.get_func_ptr() != 0)
            }),
            ..PyNumberMethods::NOT_IMPLEMENTED
        };
        &AS_NUMBER
    }
}

// CThunkObject - FFI callback (thunk) implementation

/// Userdata passed to the libffi callback.
struct ThunkUserData {
    /// The Python callable to invoke
    callable: PyObjectRef,
    /// Argument types for conversion
    arg_types: Vec<PyTypeRef>,
    /// Result type for conversion (None means void)
    pub res_type: Option<PyTypeRef>,
    /// Function flags (FUNCFLAG_USE_ERRNO, etc.)
    pub flags: u32,
}

/// Check if ty is a subclass of a simple type (like MyInt(c_int)).
fn is_simple_subclass(ty: &Py<PyType>, vm: &VirtualMachine) -> bool {
    let Ok(base) = ty.as_object().get_attr(vm.ctx.intern_str("__base__"), vm) else {
        return false;
    };
    base.get_attr(vm.ctx.intern_str("_type_"), vm).is_ok()
}

/// Convert a C value to a Python object based on the type code.
fn ffi_to_python(
    ty: &Py<PyType>,
    args: *const *const c_void,
    index: usize,
    vm: &VirtualMachine,
) -> PyObjectRef {
    let type_code = ty.type_code(vm);
    let raw_value: PyObjectRef = match unsafe {
        rustpython_host_env::ctypes::callback_arg_value_at(type_code.as_deref(), args, index)
    } {
        rustpython_host_env::ctypes::DecodedValue::Bytes(value) => vm.ctx.new_bytes(value).into(),
        rustpython_host_env::ctypes::DecodedValue::Signed(value) => vm.ctx.new_int(value).into(),
        rustpython_host_env::ctypes::DecodedValue::Unsigned(value) => vm.ctx.new_int(value).into(),
        rustpython_host_env::ctypes::DecodedValue::Float(value) => vm.ctx.new_float(value).into(),
        rustpython_host_env::ctypes::DecodedValue::Bool(value) => vm.ctx.new_bool(value).into(),
        rustpython_host_env::ctypes::DecodedValue::Pointer(value) => vm.ctx.new_int(value).into(),
        rustpython_host_env::ctypes::DecodedValue::String(value) => vm.ctx.new_str(value).into(),
        rustpython_host_env::ctypes::DecodedValue::None => vm.ctx.none(),
    };

    if !is_simple_subclass(ty, vm) {
        return raw_value;
    }
    ty.as_object()
        .call((raw_value.clone(),), vm)
        .unwrap_or(raw_value)
}

/// Convert a Python object to a C value and store it at the result pointer
fn python_to_ffi(obj: PyResult, ty: &Py<PyType>, result: *mut c_void, vm: &VirtualMachine) {
    let Ok(obj) = obj else { return };

    let type_code = ty.type_code(vm);
    match type_code.as_deref() {
        Some("b" | "h" | "i" | "l" | "q") => {
            if let Ok(i) = obj.try_int(vm) {
                unsafe {
                    rustpython_host_env::ctypes::write_callback_result(
                        type_code.as_deref(),
                        result,
                        rustpython_host_env::ctypes::CallbackResultValue::Signed(
                            i.as_bigint().to_i64().unwrap_or(0),
                        ),
                    );
                }
            }
        }
        Some("B" | "c" | "H" | "I" | "L" | "Q") => {
            if let Ok(i) = obj.try_int(vm) {
                unsafe {
                    rustpython_host_env::ctypes::write_callback_result(
                        type_code.as_deref(),
                        result,
                        rustpython_host_env::ctypes::CallbackResultValue::Unsigned(
                            i.as_bigint().to_u64().unwrap_or(0),
                        ),
                    );
                }
            }
        }
        Some("f" | "d") => {
            if let Ok(f) = obj.try_float(vm) {
                unsafe {
                    rustpython_host_env::ctypes::write_callback_result(
                        type_code.as_deref(),
                        result,
                        rustpython_host_env::ctypes::CallbackResultValue::Float(f.to_f64()),
                    );
                }
            }
        }
        Some("P" | "z" | "Z") => {
            if let Ok(i) = obj.try_int(vm) {
                unsafe {
                    rustpython_host_env::ctypes::write_callback_result(
                        type_code.as_deref(),
                        result,
                        rustpython_host_env::ctypes::CallbackResultValue::Pointer(
                            i.as_bigint().to_usize().unwrap_or(0),
                        ),
                    );
                }
            }
        }
        Some("?") => {
            if let Ok(b) = obj.is_true(vm) {
                unsafe {
                    rustpython_host_env::ctypes::write_callback_result(
                        type_code.as_deref(),
                        result,
                        rustpython_host_env::ctypes::CallbackResultValue::Bool(b),
                    );
                }
            }
        }
        _ => {}
    }
}

/// The callback function that libffi calls when the closure is invoked.
unsafe extern "C" fn thunk_callback(
    _cif: &FfiCif,
    result: &mut c_void,
    args: *const *const c_void,
    userdata: &ThunkUserData,
) {
    with_current_vm(|vm| {
        let use_errno = userdata.flags & StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0;
        let py_result =
            rustpython_host_env::ctypes::with_callback_errno_preserved(use_errno, || {
                let py_args: Vec<PyObjectRef> = userdata
                    .arg_types
                    .iter()
                    .enumerate()
                    .map(|(i, ty)| ffi_to_python(ty, args, i, vm))
                    .collect();

                userdata.callable.call(py_args, vm)
            });

        // Call unraisable hook if exception occurred
        if let Err(exc) = &py_result {
            let repr = userdata
                .callable
                .repr(vm)
                .map_or_else(|_| "<unknown>".to_string(), |s| s.to_string());
            let msg = format!("Exception ignored while calling ctypes callback function {repr}");
            vm.run_unraisable(exc.clone(), Some(msg), vm.ctx.none());
        }

        if let Some(ref res_type) = userdata.res_type {
            python_to_ffi(py_result, res_type, result as *mut c_void, vm);
        }
    });
}

/// CThunkObject wraps a Python callable to make it callable from C code.
#[pyclass(name = "CThunkObject", module = "_ctypes")]
#[derive(PyPayload)]
pub(super) struct PyCThunk {
    callable: PyObjectRef,
    #[allow(dead_code)]
    thunk_data: PyRwLock<Option<rustpython_host_env::ctypes::CallbackThunk<ThunkUserData>>>,
    code_ptr: FfiCodePtr,
}

impl Debug for PyCThunk {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("PyCThunk")
            .field("callable", &self.callable)
            .finish()
    }
}

impl PyCThunk {
    pub fn new(
        callable: PyObjectRef,
        arg_types: Option<PyObjectRef>,
        res_type: Option<PyObjectRef>,
        flags: u32,
        vm: &VirtualMachine,
    ) -> PyResult<Self> {
        let arg_type_vec: Vec<PyTypeRef> = match arg_types {
            Some(args) if !vm.is_none(&args) => args
                .try_to_value::<Vec<PyObjectRef>>(vm)?
                .into_iter()
                .map(|item| {
                    item.downcast::<PyType>()
                        .map_err(|_| vm.new_type_error("_argtypes_ must be a sequence of types"))
                })
                .collect::<PyResult<Vec<_>>>()?,
            _ => Vec::new(),
        };

        let res_type_ref: Option<PyTypeRef> = match res_type {
            Some(ref rt) if !vm.is_none(rt) => Some(
                rt.clone()
                    .downcast::<PyType>()
                    .map_err(|_| vm.new_type_error("restype must be a ctypes type"))?,
            ),
            _ => None,
        };

        let ffi_arg_types: Vec<FfiType> = arg_type_vec
            .iter()
            .map(|ty| {
                ty.type_code(vm)
                    .and_then(|code| ffi_type_from_code(&code))
                    .unwrap_or_else(ffi_pointer_type)
            })
            .collect();

        let ffi_res_type = res_type_ref
            .as_ref()
            .and_then(|ty| ty.type_code(vm))
            .and_then(|code| ffi_type_from_code(&code))
            .unwrap_or_else(ffi_void_type);

        let thunk_data = rustpython_host_env::ctypes::CallbackThunk::new(
            ffi_arg_types,
            ffi_res_type,
            Box::new(ThunkUserData {
                callable: callable.clone(),
                arg_types: arg_type_vec,
                res_type: res_type_ref,
                flags,
            }),
            thunk_callback,
        );
        let code_ptr = thunk_data.code_ptr();

        Ok(Self {
            callable,
            thunk_data: PyRwLock::new(Some(thunk_data)),
            code_ptr,
        })
    }

    pub fn code_ptr(&self) -> FfiCodePtr {
        self.code_ptr
    }
}

unsafe impl Send for PyCThunk {}
unsafe impl Sync for PyCThunk {}

#[pyclass]
impl PyCThunk {
    #[pygetset]
    fn callable(&self) -> PyObjectRef {
        self.callable.clone()
    }
}
