Files
hakorune/plugins/nyash-python-plugin/src/lib.rs

1316 lines
50 KiB
Rust
Raw Normal View History

//! Nyash Python Plugin (Phase 15):
//! - ABI v1 compatible entry points + ABI v2 TypeBox exports
//! - Two Box types: PyRuntimeBox (TYPE_ID=40) and PyObjectBox (TYPE_ID=41)
use libloading::Library;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_long, c_void};
use std::sync::{
atomic::{AtomicU32, Ordering},
Mutex,
};
// ===== Error Codes (aligned with other plugins) =====
const NYB_SUCCESS: i32 = 0;
const NYB_E_SHORT_BUFFER: i32 = -1;
const NYB_E_INVALID_TYPE: i32 = -2;
const NYB_E_INVALID_METHOD: i32 = -3;
const NYB_E_INVALID_ARGS: i32 = -4;
const NYB_E_PLUGIN_ERROR: i32 = -5;
const NYB_E_INVALID_HANDLE: i32 = -8;
// ===== Type IDs (must match nyash.toml) =====
const TYPE_ID_PY_RUNTIME: u32 = 40;
const TYPE_ID_PY_OBJECT: u32 = 41;
// ===== Method IDs (initial draft) =====
// PyRuntimeBox
const PY_METHOD_BIRTH: u32 = 0; // returns instance_id (u32 LE, no TLV)
const PY_METHOD_EVAL: u32 = 1; // args: string code -> returns Handle(PyObject)
const PY_METHOD_IMPORT: u32 = 2; // args: string name -> returns Handle(PyObject)
const PY_METHOD_FINI: u32 = u32::MAX; // destructor
// Result-returning variants (R)
const PY_METHOD_EVAL_R: u32 = 11;
const PY_METHOD_IMPORT_R: u32 = 12;
// PyObjectBox
const PYO_METHOD_BIRTH: u32 = 0; // reserved (should not be used directly)
const PYO_METHOD_GETATTR: u32 = 1; // args: string name -> returns Handle(PyObject)
const PYO_METHOD_CALL: u32 = 2; // args: variadic TLV -> returns Handle(PyObject)
const PYO_METHOD_STR: u32 = 3; // returns String
const PYO_METHOD_CALL_KW: u32 = 5; // args: key:string, val:TLV, ... -> returns Handle(PyObject)
const PYO_METHOD_FINI: u32 = u32::MAX; // destructor
// Result-returning variants (R)
const PYO_METHOD_GETATTR_R: u32 = 11;
const PYO_METHOD_CALL_R: u32 = 12;
const PYO_METHOD_CALL_KW_R: u32 = 15;
// ===== Minimal in-memory state for stubs =====
#[derive(Default)]
struct PyRuntimeInstance {
globals: Option<*mut PyObject>,
}
// Safety: Access to CPython state is guarded by the GIL in all call sites
// and we only store raw pointers captured under the GIL. We never mutate
// from multiple threads without reacquiring the GIL. Therefore, mark as
// Send/Sync for storage inside global Lazy<Mutex<...>>.
unsafe impl Send for PyRuntimeInstance {}
unsafe impl Sync for PyRuntimeInstance {}
#[derive(Default)]
struct PyObjectInstance {}
static RUNTIMES: Lazy<Mutex<HashMap<u32, PyRuntimeInstance>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
static PYOBJS: Lazy<Mutex<HashMap<u32, PyObjectInstance>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
static RT_COUNTER: AtomicU32 = AtomicU32::new(1);
static OBJ_COUNTER: AtomicU32 = AtomicU32::new(1);
// ====== Minimal dynamic CPython loader ======
type PyObject = c_void;
type PyGILState_STATE = c_int;
struct CPython {
_lib: Library,
Py_Initialize: unsafe extern "C" fn(),
Py_Finalize: unsafe extern "C" fn(),
Py_IsInitialized: unsafe extern "C" fn() -> c_int,
PyGILState_Ensure: unsafe extern "C" fn() -> PyGILState_STATE,
PyGILState_Release: unsafe extern "C" fn(PyGILState_STATE),
PyRun_StringFlags: unsafe extern "C" fn(
*const c_char,
c_int,
*mut PyObject,
*mut PyObject,
*mut c_void,
) -> *mut PyObject,
PyImport_AddModule: unsafe extern "C" fn(*const c_char) -> *mut PyObject,
PyModule_GetDict: unsafe extern "C" fn(*mut PyObject) -> *mut PyObject,
PyImport_ImportModule: unsafe extern "C" fn(*const c_char) -> *mut PyObject,
PyObject_Str: unsafe extern "C" fn(*mut PyObject) -> *mut PyObject,
PyUnicode_AsUTF8: unsafe extern "C" fn(*mut PyObject) -> *const c_char,
Py_DecRef: unsafe extern "C" fn(*mut PyObject),
Py_IncRef: unsafe extern "C" fn(*mut PyObject),
// Added for getattr/call
PyObject_GetAttrString: unsafe extern "C" fn(*mut PyObject, *const c_char) -> *mut PyObject,
PyObject_CallObject: unsafe extern "C" fn(*mut PyObject, *mut PyObject) -> *mut PyObject,
PyObject_Call:
unsafe extern "C" fn(*mut PyObject, *mut PyObject, *mut PyObject) -> *mut PyObject,
PyTuple_New: unsafe extern "C" fn(isize) -> *mut PyObject,
PyTuple_SetItem: unsafe extern "C" fn(*mut PyObject, isize, *mut PyObject) -> c_int,
PyLong_FromLongLong: unsafe extern "C" fn(i64) -> *mut PyObject,
PyUnicode_FromString: unsafe extern "C" fn(*const c_char) -> *mut PyObject,
PyBool_FromLong: unsafe extern "C" fn(c_long: c_long) -> *mut PyObject,
// Added for float/bytes and error handling
PyFloat_FromDouble: unsafe extern "C" fn(f64) -> *mut PyObject,
PyFloat_AsDouble: unsafe extern "C" fn(*mut PyObject) -> f64,
PyLong_AsLongLong: unsafe extern "C" fn(*mut PyObject) -> i64,
PyBytes_FromStringAndSize: unsafe extern "C" fn(*const c_char, isize) -> *mut PyObject,
PyBytes_AsStringAndSize:
unsafe extern "C" fn(*mut PyObject, *mut *mut c_char, *mut isize) -> c_int,
PyDict_New: unsafe extern "C" fn() -> *mut PyObject,
PyDict_SetItemString:
unsafe extern "C" fn(*mut PyObject, *const c_char, *mut PyObject) -> c_int,
PyErr_Occurred: unsafe extern "C" fn() -> *mut PyObject,
PyErr_Fetch: unsafe extern "C" fn(*mut *mut PyObject, *mut *mut PyObject, *mut *mut PyObject),
PyErr_Clear: unsafe extern "C" fn(),
}
static CPY: Lazy<Mutex<Option<CPython>>> = Lazy::new(|| Mutex::new(None));
fn try_load_cpython() -> Result<(), ()> {
let mut candidates: Vec<String> = vec![
// Linux/WSL common
"libpython3.12.so".into(),
"libpython3.12.so.1.0".into(),
"libpython3.11.so".into(),
"libpython3.11.so.1.0".into(),
"libpython3.10.so".into(),
"libpython3.10.so.1.0".into(),
"libpython3.9.so".into(),
"libpython3.9.so.1.0".into(),
// macOS
"libpython3.12.dylib".into(),
"libpython3.11.dylib".into(),
"libpython3.10.dylib".into(),
"libpython3.9.dylib".into(),
];
// Windows DLLs (search via PATH / System32)
if cfg!(target_os = "windows") {
let dlls = [
"python312.dll",
"python311.dll",
"python310.dll",
"python39.dll",
];
for d in dlls.iter() {
candidates.push((*d).into());
}
if let Ok(pyhome) = std::env::var("PYTHONHOME") {
for d in dlls.iter() {
let p = std::path::Path::new(&pyhome).join(d);
if p.exists() {
candidates.push(p.to_string_lossy().to_string());
}
}
}
}
for name in candidates.into_iter() {
if let Ok(lib) = unsafe { Library::new(&name) } {
unsafe {
let Py_Initialize = *lib
.get::<unsafe extern "C" fn()>(b"Py_Initialize\0")
.map_err(|_| ())?;
let Py_Finalize = *lib
.get::<unsafe extern "C" fn()>(b"Py_Finalize\0")
.map_err(|_| ())?;
let Py_IsInitialized = *lib
.get::<unsafe extern "C" fn() -> c_int>(b"Py_IsInitialized\0")
.map_err(|_| ())?;
let PyGILState_Ensure = *lib
.get::<unsafe extern "C" fn() -> PyGILState_STATE>(b"PyGILState_Ensure\0")
.map_err(|_| ())?;
let PyGILState_Release = *lib
.get::<unsafe extern "C" fn(PyGILState_STATE)>(b"PyGILState_Release\0")
.map_err(|_| ())?;
let PyRun_StringFlags = *lib
.get::<unsafe extern "C" fn(
*const c_char,
c_int,
*mut PyObject,
*mut PyObject,
*mut c_void,
) -> *mut PyObject>(b"PyRun_StringFlags\0")
.map_err(|_| ())?;
let PyImport_AddModule = *lib
.get::<unsafe extern "C" fn(*const c_char) -> *mut PyObject>(
b"PyImport_AddModule\0",
)
.map_err(|_| ())?;
let PyModule_GetDict = *lib
.get::<unsafe extern "C" fn(*mut PyObject) -> *mut PyObject>(
b"PyModule_GetDict\0",
)
.map_err(|_| ())?;
let PyImport_ImportModule = *lib
.get::<unsafe extern "C" fn(*const c_char) -> *mut PyObject>(
b"PyImport_ImportModule\0",
)
.map_err(|_| ())?;
let PyObject_Str = *lib
.get::<unsafe extern "C" fn(*mut PyObject) -> *mut PyObject>(b"PyObject_Str\0")
.map_err(|_| ())?;
let PyUnicode_AsUTF8 = *lib
.get::<unsafe extern "C" fn(*mut PyObject) -> *const c_char>(
b"PyUnicode_AsUTF8\0",
)
.map_err(|_| ())?;
let Py_DecRef = *lib
.get::<unsafe extern "C" fn(*mut PyObject)>(b"Py_DecRef\0")
.map_err(|_| ())?;
let Py_IncRef = *lib
.get::<unsafe extern "C" fn(*mut PyObject)>(b"Py_IncRef\0")
.map_err(|_| ())?;
let PyObject_GetAttrString = *lib
.get::<unsafe extern "C" fn(*mut PyObject, *const c_char) -> *mut PyObject>(
b"PyObject_GetAttrString\0",
)
.map_err(|_| ())?;
let PyObject_CallObject = *lib
.get::<unsafe extern "C" fn(*mut PyObject, *mut PyObject) -> *mut PyObject>(
b"PyObject_CallObject\0",
)
.map_err(|_| ())?;
let PyObject_Call = *lib
.get::<unsafe extern "C" fn(
*mut PyObject,
*mut PyObject,
*mut PyObject,
) -> *mut PyObject>(b"PyObject_Call\0")
.map_err(|_| ())?;
let PyTuple_New = *lib
.get::<unsafe extern "C" fn(isize) -> *mut PyObject>(b"PyTuple_New\0")
.map_err(|_| ())?;
let PyTuple_SetItem = *lib
.get::<unsafe extern "C" fn(*mut PyObject, isize, *mut PyObject) -> c_int>(
b"PyTuple_SetItem\0",
)
.map_err(|_| ())?;
let PyLong_FromLongLong = *lib
.get::<unsafe extern "C" fn(i64) -> *mut PyObject>(b"PyLong_FromLongLong\0")
.map_err(|_| ())?;
let PyUnicode_FromString = *lib
.get::<unsafe extern "C" fn(*const c_char) -> *mut PyObject>(
b"PyUnicode_FromString\0",
)
.map_err(|_| ())?;
let PyBool_FromLong = *lib
.get::<unsafe extern "C" fn(c_long: c_long) -> *mut PyObject>(
b"PyBool_FromLong\0",
)
.map_err(|_| ())?;
let PyFloat_FromDouble = *lib
.get::<unsafe extern "C" fn(f64) -> *mut PyObject>(b"PyFloat_FromDouble\0")
.map_err(|_| ())?;
let PyFloat_AsDouble = *lib
.get::<unsafe extern "C" fn(*mut PyObject) -> f64>(b"PyFloat_AsDouble\0")
.map_err(|_| ())?;
let PyLong_AsLongLong = *lib
.get::<unsafe extern "C" fn(*mut PyObject) -> i64>(b"PyLong_AsLongLong\0")
.map_err(|_| ())?;
let PyBytes_FromStringAndSize = *lib
.get::<unsafe extern "C" fn(*const c_char, isize) -> *mut PyObject>(
b"PyBytes_FromStringAndSize\0",
)
.map_err(|_| ())?;
let PyBytes_AsStringAndSize = *lib.get::<unsafe extern "C" fn(*mut PyObject, *mut *mut c_char, *mut isize) -> c_int>(b"PyBytes_AsStringAndSize\0").map_err(|_| ())?;
let PyErr_Occurred = *lib
.get::<unsafe extern "C" fn() -> *mut PyObject>(b"PyErr_Occurred\0")
.map_err(|_| ())?;
let PyDict_New = *lib
.get::<unsafe extern "C" fn() -> *mut PyObject>(b"PyDict_New\0")
.map_err(|_| ())?;
let PyDict_SetItemString = *lib.get::<unsafe extern "C" fn(*mut PyObject, *const c_char, *mut PyObject) -> c_int>(b"PyDict_SetItemString\0").map_err(|_| ())?;
let PyErr_Fetch =
*lib.get::<unsafe extern "C" fn(
*mut *mut PyObject,
*mut *mut PyObject,
*mut *mut PyObject,
)>(b"PyErr_Fetch\0")
.map_err(|_| ())?;
let PyErr_Clear = *lib
.get::<unsafe extern "C" fn()>(b"PyErr_Clear\0")
.map_err(|_| ())?;
let cpy = CPython {
_lib: lib,
Py_Initialize,
Py_Finalize,
Py_IsInitialized,
PyGILState_Ensure,
PyGILState_Release,
PyRun_StringFlags,
PyImport_AddModule,
PyModule_GetDict,
PyImport_ImportModule,
PyObject_Str,
PyUnicode_AsUTF8,
Py_DecRef,
Py_IncRef,
PyObject_GetAttrString,
PyObject_CallObject,
PyObject_Call,
PyTuple_New,
PyTuple_SetItem,
PyLong_FromLongLong,
PyUnicode_FromString,
PyBool_FromLong,
PyFloat_FromDouble,
PyFloat_AsDouble,
PyLong_AsLongLong,
PyBytes_FromStringAndSize,
PyBytes_AsStringAndSize,
PyErr_Occurred,
PyDict_New,
PyDict_SetItemString,
PyErr_Fetch,
PyErr_Clear,
};
*CPY.lock().unwrap() = Some(cpy);
return Ok(());
}
}
}
Err(())
}
fn ensure_cpython() -> Result<(), ()> {
if CPY.lock().unwrap().is_none() {
try_load_cpython()?;
// Initialize on first load
unsafe {
if let Some(cpy) = &*CPY.lock().unwrap() {
if (cpy.Py_IsInitialized)() == 0 {
(cpy.Py_Initialize)();
}
}
}
}
Ok(())
}
#[no_mangle]
pub extern "C" fn nyash_plugin_abi() -> u32 {
1
}
#[no_mangle]
pub extern "C" fn nyash_plugin_init() -> i32 {
NYB_SUCCESS
}
#[no_mangle]
pub extern "C" fn nyash_plugin_invoke(
type_id: u32,
method_id: u32,
instance_id: u32,
args: *const u8,
args_len: usize,
result: *mut u8,
result_len: *mut usize,
) -> i32 {
match type_id {
TYPE_ID_PY_RUNTIME => {
handle_py_runtime(method_id, instance_id, args, args_len, result, result_len)
}
TYPE_ID_PY_OBJECT => {
handle_py_object(method_id, instance_id, args, args_len, result, result_len)
}
_ => NYB_E_INVALID_TYPE,
}
}
fn handle_py_runtime(
method_id: u32,
_instance_id: u32,
_args: *const u8,
_args_len: usize,
result: *mut u8,
result_len: *mut usize,
) -> i32 {
unsafe {
match method_id {
PY_METHOD_BIRTH => {
if result_len.is_null() {
return NYB_E_INVALID_ARGS;
}
if preflight(result, result_len, 4) {
return NYB_E_SHORT_BUFFER;
}
if ensure_cpython().is_err() {
return NYB_E_PLUGIN_ERROR;
}
let id = RT_COUNTER.fetch_add(1, Ordering::Relaxed);
let mut inst = PyRuntimeInstance::default();
if let Some(cpy) = &*CPY.lock().unwrap() {
let c_main = CString::new("__main__").unwrap();
let state = (cpy.PyGILState_Ensure)();
let module = (cpy.PyImport_AddModule)(c_main.as_ptr());
if !module.is_null() {
let dict = (cpy.PyModule_GetDict)(module);
if !dict.is_null() {
inst.globals = Some(dict);
}
}
(cpy.PyGILState_Release)(state);
}
if let Ok(mut map) = RUNTIMES.lock() {
map.insert(id, inst);
} else {
return NYB_E_PLUGIN_ERROR;
}
let bytes = id.to_le_bytes();
std::ptr::copy_nonoverlapping(bytes.as_ptr(), result, 4);
*result_len = 4;
NYB_SUCCESS
}
PY_METHOD_FINI => {
// Only drop runtime slot; avoid calling Py_Finalize to prevent shutdown crashes.
if let Ok(mut map) = RUNTIMES.lock() {
map.remove(&_instance_id);
}
NYB_SUCCESS
}
PY_METHOD_EVAL | PY_METHOD_EVAL_R => {
if ensure_cpython().is_err() {
return NYB_E_PLUGIN_ERROR;
}
// Allow zero-arg eval by reading from env var (NYASH_PY_EVAL_CODE) for bootstrap demos
let argc = tlv_count_args(_args, _args_len);
let code = if argc == 0 {
std::env::var("NYASH_PY_EVAL_CODE").unwrap_or_else(|_| "".to_string())
} else {
if let Some(s) = read_arg_string(_args, _args_len, 0) {
s
} else {
return NYB_E_INVALID_ARGS;
}
};
let c_code = match CString::new(code) {
Ok(s) => s,
Err(_) => return NYB_E_INVALID_ARGS,
};
if let Some(cpy) = &*CPY.lock().unwrap() {
let state = (cpy.PyGILState_Ensure)();
// use per-runtime globals if available
let mut dict: *mut PyObject = std::ptr::null_mut();
if let Ok(map) = RUNTIMES.lock() {
if let Some(rt) = map.get(&_instance_id) {
if let Some(g) = rt.globals {
dict = g;
}
}
}
if dict.is_null() {
let c_main = CString::new("__main__").unwrap();
let module = (cpy.PyImport_AddModule)(c_main.as_ptr());
if module.is_null() {
(cpy.PyGILState_Release)(state);
return NYB_E_PLUGIN_ERROR;
}
dict = (cpy.PyModule_GetDict)(module);
}
// 258 == Py_eval_input
let obj = (cpy.PyRun_StringFlags)(
c_code.as_ptr(),
258,
dict,
dict,
std::ptr::null_mut(),
);
if obj.is_null() {
let msg = take_py_error_string(cpy);
(cpy.PyGILState_Release)(state);
if method_id == PY_METHOD_EVAL_R {
return NYB_E_PLUGIN_ERROR;
}
if let Some(m) = msg {
return write_tlv_string(&m, result, result_len);
}
return NYB_E_PLUGIN_ERROR;
}
if (method_id == PY_METHOD_EVAL || method_id == PY_METHOD_EVAL_R)
&& should_autodecode()
&& try_write_autodecode(cpy, obj, result, result_len)
{
(cpy.Py_DecRef)(obj);
(cpy.PyGILState_Release)(state);
return NYB_SUCCESS;
}
// Store as PyObjectBox handle
let id = OBJ_COUNTER.fetch_add(1, Ordering::Relaxed);
if let Ok(mut map) = PYOBJS.lock() {
map.insert(id, PyObjectInstance::default());
} else {
(cpy.Py_DecRef)(obj);
(cpy.PyGILState_Release)(state);
return NYB_E_PLUGIN_ERROR;
}
// Keep reference (obj is new ref). We model store via separate map and hold pointer via raw address table.
// To actually manage pointer per id, we extend PyObjectInstance in 10.5b with a field. For now, attach through side-table.
OBJ_PTRS.lock().unwrap().insert(id, PyPtr(obj));
(cpy.PyGILState_Release)(state);
return write_tlv_handle(TYPE_ID_PY_OBJECT, id, result, result_len);
}
NYB_E_PLUGIN_ERROR
}
PY_METHOD_IMPORT | PY_METHOD_IMPORT_R => {
if ensure_cpython().is_err() {
return NYB_E_PLUGIN_ERROR;
}
let Some(name) = read_arg_string(_args, _args_len, 0) else {
return NYB_E_INVALID_ARGS;
};
let c_name = match CString::new(name) {
Ok(s) => s,
Err(_) => return NYB_E_INVALID_ARGS,
};
if let Some(cpy) = &*CPY.lock().unwrap() {
let state = (cpy.PyGILState_Ensure)();
let obj = (cpy.PyImport_ImportModule)(c_name.as_ptr());
if obj.is_null() {
let msg = take_py_error_string(cpy);
(cpy.PyGILState_Release)(state);
if method_id == PY_METHOD_IMPORT_R {
return NYB_E_PLUGIN_ERROR;
}
if let Some(m) = msg {
return write_tlv_string(&m, result, result_len);
}
return NYB_E_PLUGIN_ERROR;
}
// expose module into runtime globals
if let Ok(map) = RUNTIMES.lock() {
if let Some(rt) = map.get(&_instance_id) {
if let Some(gl) = rt.globals {
(cpy.PyDict_SetItemString)(gl, c_name.as_ptr(), obj);
}
}
}
let id = OBJ_COUNTER.fetch_add(1, Ordering::Relaxed);
if let Ok(mut map) = PYOBJS.lock() {
map.insert(id, PyObjectInstance::default());
} else {
(cpy.Py_DecRef)(obj);
(cpy.PyGILState_Release)(state);
return NYB_E_PLUGIN_ERROR;
}
OBJ_PTRS.lock().unwrap().insert(id, PyPtr(obj));
(cpy.PyGILState_Release)(state);
return write_tlv_handle(TYPE_ID_PY_OBJECT, id, result, result_len);
}
NYB_E_PLUGIN_ERROR
}
_ => NYB_E_INVALID_METHOD,
}
}
}
fn handle_py_object(
method_id: u32,
instance_id: u32,
_args: *const u8,
_args_len: usize,
result: *mut u8,
result_len: *mut usize,
) -> i32 {
match method_id {
PYO_METHOD_BIRTH => NYB_E_INVALID_METHOD, // should be created via runtime
PYO_METHOD_FINI => {
if let Some(cpy) = &*CPY.lock().unwrap() {
if let Some(ptr) = OBJ_PTRS.lock().unwrap().remove(&instance_id) {
unsafe {
(cpy.Py_DecRef)(ptr.0);
}
}
}
if let Ok(mut map) = PYOBJS.lock() {
map.remove(&instance_id);
NYB_SUCCESS
} else {
NYB_E_PLUGIN_ERROR
}
}
PYO_METHOD_GETATTR | PYO_METHOD_GETATTR_R => {
if ensure_cpython().is_err() {
return NYB_E_PLUGIN_ERROR;
}
let Some(name) = read_arg_string(_args, _args_len, 0) else {
return NYB_E_INVALID_ARGS;
};
if let Some(cpy) = &*CPY.lock().unwrap() {
let Some(obj) = OBJ_PTRS.lock().unwrap().get(&instance_id).copied() else {
return NYB_E_INVALID_HANDLE;
};
let c_name = match CString::new(name) {
Ok(s) => s,
Err(_) => return NYB_E_INVALID_ARGS,
};
let state = unsafe { (cpy.PyGILState_Ensure)() };
let attr = unsafe { (cpy.PyObject_GetAttrString)(obj.0, c_name.as_ptr()) };
if attr.is_null() {
let msg = take_py_error_string(cpy);
unsafe {
(cpy.PyGILState_Release)(state);
}
if method_id == PYO_METHOD_GETATTR_R {
return NYB_E_PLUGIN_ERROR;
}
if let Some(m) = msg {
return write_tlv_string(&m, result, result_len);
}
return NYB_E_PLUGIN_ERROR;
}
if should_autodecode() && try_write_autodecode(cpy, attr, result, result_len) {
unsafe {
(cpy.Py_DecRef)(attr);
(cpy.PyGILState_Release)(state);
}
return NYB_SUCCESS;
}
let id = OBJ_COUNTER.fetch_add(1, Ordering::Relaxed);
OBJ_PTRS.lock().unwrap().insert(id, PyPtr(attr));
unsafe {
(cpy.PyGILState_Release)(state);
}
return write_tlv_handle(TYPE_ID_PY_OBJECT, id, result, result_len);
}
NYB_E_PLUGIN_ERROR
}
PYO_METHOD_CALL | PYO_METHOD_CALL_R => {
if ensure_cpython().is_err() {
return NYB_E_PLUGIN_ERROR;
}
if let Some(cpy) = &*CPY.lock().unwrap() {
let Some(func) = OBJ_PTRS.lock().unwrap().get(&instance_id).copied() else {
return NYB_E_INVALID_HANDLE;
};
let state = unsafe { (cpy.PyGILState_Ensure)() };
// Build tuple from TLV args
let argc = tlv_count_args(_args, _args_len);
let tuple = unsafe { (cpy.PyTuple_New)(argc as isize) };
if tuple.is_null() {
unsafe {
(cpy.PyGILState_Release)(state);
}
return NYB_E_PLUGIN_ERROR;
}
if !fill_tuple_from_tlv(cpy, tuple, _args, _args_len) {
unsafe {
(cpy.Py_DecRef)(tuple);
(cpy.PyGILState_Release)(state);
}
return NYB_E_INVALID_ARGS;
}
let ret = unsafe { (cpy.PyObject_CallObject)(func.0, tuple) };
unsafe {
(cpy.Py_DecRef)(tuple);
}
if ret.is_null() {
let msg = take_py_error_string(cpy);
unsafe {
(cpy.PyGILState_Release)(state);
}
if method_id == PYO_METHOD_CALL_R {
return NYB_E_PLUGIN_ERROR;
}
if let Some(m) = msg {
return write_tlv_string(&m, result, result_len);
}
return NYB_E_PLUGIN_ERROR;
}
if should_autodecode() && try_write_autodecode(cpy, ret, result, result_len) {
unsafe {
(cpy.Py_DecRef)(ret);
(cpy.PyGILState_Release)(state);
}
return NYB_SUCCESS;
}
let id = OBJ_COUNTER.fetch_add(1, Ordering::Relaxed);
OBJ_PTRS.lock().unwrap().insert(id, PyPtr(ret));
unsafe {
(cpy.PyGILState_Release)(state);
}
return write_tlv_handle(TYPE_ID_PY_OBJECT, id, result, result_len);
}
NYB_E_PLUGIN_ERROR
}
PYO_METHOD_CALL_KW | PYO_METHOD_CALL_KW_R => {
if ensure_cpython().is_err() {
return NYB_E_PLUGIN_ERROR;
}
if let Some(cpy) = &*CPY.lock().unwrap() {
let Some(func) = OBJ_PTRS.lock().unwrap().get(&instance_id).copied() else {
return NYB_E_INVALID_HANDLE;
};
let state = unsafe { (cpy.PyGILState_Ensure)() };
// Empty args tuple for kwargs-only call
let args_tup = unsafe { (cpy.PyTuple_New)(0) };
if args_tup.is_null() {
unsafe {
(cpy.PyGILState_Release)(state);
}
return NYB_E_PLUGIN_ERROR;
}
// Build kwargs dict from TLV pairs
let kwargs = unsafe { (cpy.PyDict_New)() };
if kwargs.is_null() {
unsafe {
(cpy.Py_DecRef)(args_tup);
(cpy.PyGILState_Release)(state);
}
return NYB_E_PLUGIN_ERROR;
}
if !fill_kwargs_from_tlv(cpy, kwargs, _args, _args_len) {
unsafe {
(cpy.Py_DecRef)(kwargs);
(cpy.Py_DecRef)(args_tup);
(cpy.PyGILState_Release)(state);
}
return NYB_E_INVALID_ARGS;
}
let ret = unsafe { (cpy.PyObject_Call)(func.0, args_tup, kwargs) };
unsafe {
(cpy.Py_DecRef)(kwargs);
(cpy.Py_DecRef)(args_tup);
}
if ret.is_null() {
let msg = take_py_error_string(cpy);
unsafe {
(cpy.PyGILState_Release)(state);
}
if method_id == PYO_METHOD_CALL_KW_R {
return NYB_E_PLUGIN_ERROR;
}
if let Some(m) = msg {
return write_tlv_string(&m, result, result_len);
}
return NYB_E_PLUGIN_ERROR;
}
if (method_id == PYO_METHOD_CALL_KW || method_id == PYO_METHOD_CALL_KW_R)
&& should_autodecode()
&& try_write_autodecode(cpy, ret, result, result_len)
{
unsafe {
(cpy.Py_DecRef)(ret);
(cpy.PyGILState_Release)(state);
}
return NYB_SUCCESS;
}
let id = OBJ_COUNTER.fetch_add(1, Ordering::Relaxed);
OBJ_PTRS.lock().unwrap().insert(id, PyPtr(ret));
unsafe {
(cpy.PyGILState_Release)(state);
}
return write_tlv_handle(TYPE_ID_PY_OBJECT, id, result, result_len);
}
NYB_E_PLUGIN_ERROR
}
PYO_METHOD_STR => {
if ensure_cpython().is_err() {
return NYB_E_PLUGIN_ERROR;
}
if let Some(cpy) = &*CPY.lock().unwrap() {
let Some(obj) = OBJ_PTRS.lock().unwrap().get(&instance_id).copied() else {
return NYB_E_INVALID_HANDLE;
};
let state = unsafe { (cpy.PyGILState_Ensure)() };
let s_obj = unsafe { (cpy.PyObject_Str)(obj.0) };
if s_obj.is_null() {
unsafe {
(cpy.PyGILState_Release)(state);
}
return NYB_E_PLUGIN_ERROR;
}
let cstr = unsafe { (cpy.PyUnicode_AsUTF8)(s_obj) };
if cstr.is_null() {
unsafe {
(cpy.Py_DecRef)(s_obj);
(cpy.PyGILState_Release)(state);
}
return NYB_E_PLUGIN_ERROR;
}
let rust_str = unsafe { CStr::from_ptr(cstr) }
.to_string_lossy()
.to_string();
unsafe {
(cpy.Py_DecRef)(s_obj);
(cpy.PyGILState_Release)(state);
}
return write_tlv_string(&rust_str, result, result_len);
}
NYB_E_PLUGIN_ERROR
}
// keep others unimplemented in 10.5b-min
_ => NYB_E_INVALID_METHOD,
}
}
// ===== Minimal TLV helpers (copy from other plugins for consistency) =====
// ===== TypeBox ABI v2 (resolve/invoke_id) =====
#[repr(C)]
pub struct NyashTypeBoxFfi {
pub abi_tag: u32, // 'TYBX'
pub version: u16, // 1
pub struct_size: u16, // sizeof(NyashTypeBoxFfi)
pub name: *const std::os::raw::c_char,
pub resolve: Option<extern "C" fn(*const std::os::raw::c_char) -> u32>,
pub invoke_id: Option<extern "C" fn(u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32>,
pub capabilities: u64,
}
unsafe impl Sync for NyashTypeBoxFfi {}
extern "C" fn pyruntime_resolve(name: *const std::os::raw::c_char) -> u32 {
if name.is_null() { return 0; }
let s = unsafe { CStr::from_ptr(name) }.to_string_lossy();
match s.as_ref() {
"birth" => PY_METHOD_BIRTH,
"eval" | "evalR" => { if s.as_ref() == "evalR" { PY_METHOD_EVAL_R } else { PY_METHOD_EVAL } }
"import" | "importR" => { if s.as_ref() == "importR" { PY_METHOD_IMPORT_R } else { PY_METHOD_IMPORT } }
"fini" => PY_METHOD_FINI,
_ => 0,
}
}
extern "C" fn pyruntime_invoke_id(
instance_id: u32,
method_id: u32,
args: *const u8,
args_len: usize,
result: *mut u8,
result_len: *mut usize,
) -> i32 {
handle_py_runtime(method_id, instance_id, args, args_len, result, result_len)
}
extern "C" fn pyobject_resolve(name: *const std::os::raw::c_char) -> u32 {
if name.is_null() { return 0; }
let s = unsafe { CStr::from_ptr(name) }.to_string_lossy();
match s.as_ref() {
"getattr" | "getAttr" | "getattrR" | "getAttrR" => {
if s.ends_with('R') { PYO_METHOD_GETATTR_R } else { PYO_METHOD_GETATTR }
}
"call" | "callR" => { if s.ends_with('R') { PYO_METHOD_CALL_R } else { PYO_METHOD_CALL } }
"callKw" | "callKW" | "call_kw" | "callKwR" | "callKWR" => {
if s.to_lowercase().ends_with('r') { PYO_METHOD_CALL_KW_R } else { PYO_METHOD_CALL_KW }
}
"str" | "toString" => PYO_METHOD_STR,
"birth" => PYO_METHOD_BIRTH,
"fini" => PYO_METHOD_FINI,
_ => 0,
}
}
extern "C" fn pyobject_invoke_id(
instance_id: u32,
method_id: u32,
args: *const u8,
args_len: usize,
result: *mut u8,
result_len: *mut usize,
) -> i32 {
handle_py_object(method_id, instance_id, args, args_len, result, result_len)
}
#[no_mangle]
pub static nyash_typebox_PyRuntimeBox: NyashTypeBoxFfi = NyashTypeBoxFfi {
abi_tag: 0x54594258,
version: 1,
struct_size: std::mem::size_of::<NyashTypeBoxFfi>() as u16,
name: b"PyRuntimeBox\0".as_ptr() as *const std::os::raw::c_char,
resolve: Some(pyruntime_resolve),
invoke_id: Some(pyruntime_invoke_id),
capabilities: 0,
};
#[no_mangle]
pub static nyash_typebox_PyObjectBox: NyashTypeBoxFfi = NyashTypeBoxFfi {
abi_tag: 0x54594258,
version: 1,
struct_size: std::mem::size_of::<NyashTypeBoxFfi>() as u16,
name: b"PyObjectBox\0".as_ptr() as *const std::os::raw::c_char,
resolve: Some(pyobject_resolve),
invoke_id: Some(pyobject_invoke_id),
capabilities: 0,
};
fn preflight(result: *mut u8, result_len: *mut usize, needed: usize) -> bool {
unsafe {
if result_len.is_null() {
return false;
}
if result.is_null() || *result_len < needed {
*result_len = needed;
return true;
}
}
false
}
fn write_tlv_string(s: &str, result: *mut u8, result_len: *mut usize) -> i32 {
let payload = s.as_bytes();
write_tlv_result(&[(6u8, payload)], result, result_len)
}
fn write_tlv_handle(
type_id: u32,
instance_id: u32,
result: *mut u8,
result_len: *mut usize,
) -> i32 {
let mut payload = [0u8; 8];
payload[..4].copy_from_slice(&type_id.to_le_bytes());
payload[4..].copy_from_slice(&instance_id.to_le_bytes());
write_tlv_result(&[(8u8, &payload)], result, result_len)
}
/// Read nth TLV argument as String (tag 6)
fn read_arg_string(args: *const u8, args_len: usize, n: usize) -> Option<String> {
if args.is_null() || args_len < 4 {
return None;
}
let buf = unsafe { std::slice::from_raw_parts(args, args_len) };
let mut off = 4usize; // skip header
for i in 0..=n {
if buf.len() < off + 4 {
return None;
}
let tag = buf[off];
let _rsv = buf[off + 1];
let size = u16::from_le_bytes([buf[off + 2], buf[off + 3]]) as usize;
if buf.len() < off + 4 + size {
return None;
}
if i == n {
if tag != 6 {
return None;
}
let slice = &buf[off + 4..off + 4 + size];
return std::str::from_utf8(slice).ok().map(|s| s.to_string());
}
off += 4 + size;
}
None
}
// Side-table for PyObject* storage (instance_id -> pointer)
#[derive(Copy, Clone)]
struct PyPtr(*mut PyObject);
unsafe impl Send for PyPtr {}
unsafe impl Sync for PyPtr {}
static OBJ_PTRS: Lazy<Mutex<HashMap<u32, PyPtr>>> = Lazy::new(|| Mutex::new(HashMap::new()));
// Base TLV writer used by helpers
fn write_tlv_result(payloads: &[(u8, &[u8])], result: *mut u8, result_len: *mut usize) -> i32 {
if result_len.is_null() {
return NYB_E_INVALID_ARGS;
}
let mut buf: Vec<u8> =
Vec::with_capacity(4 + payloads.iter().map(|(_, p)| 4 + p.len()).sum::<usize>());
buf.extend_from_slice(&1u16.to_le_bytes()); // version
buf.extend_from_slice(&(payloads.len() as u16).to_le_bytes()); // argc
for (tag, payload) in payloads {
buf.push(*tag);
buf.push(0);
buf.extend_from_slice(&(payload.len() as u16).to_le_bytes());
buf.extend_from_slice(payload);
}
unsafe {
let needed = buf.len();
if result.is_null() || *result_len < needed {
*result_len = needed;
return NYB_E_SHORT_BUFFER;
}
std::ptr::copy_nonoverlapping(buf.as_ptr(), result, needed);
*result_len = needed;
}
NYB_SUCCESS
}
fn tlv_count_args(args: *const u8, args_len: usize) -> usize {
if args.is_null() || args_len < 4 {
return 0;
}
let buf = unsafe { std::slice::from_raw_parts(args, args_len) };
if buf.len() < 4 {
return 0;
}
let argc = u16::from_le_bytes([buf[2], buf[3]]) as usize;
argc
}
fn fill_tuple_from_tlv(
cpy: &CPython,
tuple: *mut PyObject,
args: *const u8,
args_len: usize,
) -> bool {
if args.is_null() || args_len < 4 {
return true;
}
let buf = unsafe { std::slice::from_raw_parts(args, args_len) };
let mut off = 4usize;
let mut idx: isize = 0;
while off + 4 <= buf.len() {
let tag = buf[off];
let _rsv = buf[off + 1];
let size = u16::from_le_bytes([buf[off + 2], buf[off + 3]]) as usize;
if off + 4 + size > buf.len() {
return false;
}
let payload = &buf[off + 4..off + 4 + size];
let mut obj: *mut PyObject = std::ptr::null_mut();
unsafe {
match tag {
1 => {
// Bool (1 byte)
let v = if size >= 1 && payload[0] != 0 { 1 } else { 0 };
obj = (cpy.PyBool_FromLong)(v);
}
2 => {
// I32
if size != 4 {
return false;
}
let mut b = [0u8; 4];
b.copy_from_slice(payload);
let v = i32::from_le_bytes(b) as i64;
obj = (cpy.PyLong_FromLongLong)(v);
}
3 => {
// I64
if size != 8 {
return false;
}
let mut b = [0u8; 8];
b.copy_from_slice(payload);
let v = i64::from_le_bytes(b);
obj = (cpy.PyLong_FromLongLong)(v);
}
5 => {
// F64
if size != 8 {
return false;
}
let mut b = [0u8; 8];
b.copy_from_slice(payload);
let v = f64::from_le_bytes(b);
obj = (cpy.PyFloat_FromDouble)(v);
}
6 => {
// String
let c = match CString::new(payload) {
Ok(c) => c,
Err(_) => return false,
};
obj = (cpy.PyUnicode_FromString)(c.as_ptr());
}
7 => {
// Bytes
if size > 0 {
let ptr = payload.as_ptr() as *const c_char;
obj = (cpy.PyBytes_FromStringAndSize)(ptr, size as isize);
} else {
obj = (cpy.PyBytes_FromStringAndSize)(std::ptr::null(), 0);
}
}
8 => {
// Handle
if size != 8 {
return false;
}
let mut t = [0u8; 4];
t.copy_from_slice(&payload[0..4]);
let mut i = [0u8; 4];
i.copy_from_slice(&payload[4..8]);
let _type_id = u32::from_le_bytes(t);
let inst_id = u32::from_le_bytes(i);
if let Some(ptr) = OBJ_PTRS.lock().unwrap().get(&inst_id).copied() {
// PyTuple_SetItem steals a reference; INCREF to keep original alive
(cpy.Py_IncRef)(ptr.0);
obj = ptr.0;
} else {
return false;
}
}
_ => {
return false;
}
}
if obj.is_null() {
return false;
}
let rc = (cpy.PyTuple_SetItem)(tuple, idx, obj);
if rc != 0 {
return false;
}
idx += 1;
}
off += 4 + size;
}
true
}
fn take_py_error_string(cpy: &CPython) -> Option<String> {
unsafe {
if (cpy.PyErr_Occurred)().is_null() {
return None;
}
let mut ptype: *mut PyObject = std::ptr::null_mut();
let mut pvalue: *mut PyObject = std::ptr::null_mut();
let mut ptrace: *mut PyObject = std::ptr::null_mut();
(cpy.PyErr_Fetch)(&mut ptype, &mut pvalue, &mut ptrace);
let s = if !pvalue.is_null() {
let sobj = (cpy.PyObject_Str)(pvalue);
if sobj.is_null() {
(cpy.PyErr_Clear)();
return Some("Python error".to_string());
}
let cstr = (cpy.PyUnicode_AsUTF8)(sobj);
let msg = if cstr.is_null() {
"Python error".to_string()
} else {
CStr::from_ptr(cstr).to_string_lossy().to_string()
};
(cpy.Py_DecRef)(sobj);
msg
} else {
"Python error".to_string()
};
(cpy.PyErr_Clear)();
Some(s)
}
}
fn fill_kwargs_from_tlv(
cpy: &CPython,
dict: *mut PyObject,
args: *const u8,
args_len: usize,
) -> bool {
if args.is_null() || args_len < 4 {
return true;
}
let buf = unsafe { std::slice::from_raw_parts(args, args_len) };
let mut off = 4usize;
loop {
if off + 4 > buf.len() {
break;
}
let tag_k = buf[off];
let _rsv = buf[off + 1];
let size_k = u16::from_le_bytes([buf[off + 2], buf[off + 3]]) as usize;
if tag_k != 6 || off + 4 + size_k > buf.len() {
return false;
}
let key_bytes = &buf[off + 4..off + 4 + size_k];
off += 4 + size_k;
if off + 4 > buf.len() {
return false;
}
let tag_v = buf[off];
let _rsv2 = buf[off + 1];
let size_v = u16::from_le_bytes([buf[off + 2], buf[off + 3]]) as usize;
if off + 4 + size_v > buf.len() {
return false;
}
let val_payload = &buf[off + 4..off + 4 + size_v];
off += 4 + size_v;
unsafe {
let key_c = match CString::new(key_bytes) {
Ok(c) => c,
Err(_) => return false,
};
let mut obj: *mut PyObject = std::ptr::null_mut();
match tag_v {
1 => {
let v = if size_v >= 1 && val_payload[0] != 0 {
1
} else {
0
};
obj = (cpy.PyBool_FromLong)(v);
}
2 => {
if size_v != 4 {
return false;
}
let mut b = [0u8; 4];
b.copy_from_slice(val_payload);
obj = (cpy.PyLong_FromLongLong)(i32::from_le_bytes(b) as i64);
}
3 => {
if size_v != 8 {
return false;
}
let mut b = [0u8; 8];
b.copy_from_slice(val_payload);
obj = (cpy.PyLong_FromLongLong)(i64::from_le_bytes(b));
}
5 => {
if size_v != 8 {
return false;
}
let mut b = [0u8; 8];
b.copy_from_slice(val_payload);
obj = (cpy.PyFloat_FromDouble)(f64::from_le_bytes(b));
}
6 => {
let c = match CString::new(val_payload) {
Ok(c) => c,
Err(_) => return false,
};
obj = (cpy.PyUnicode_FromString)(c.as_ptr());
}
7 => {
let ptr = if size_v > 0 {
val_payload.as_ptr() as *const c_char
} else {
std::ptr::null()
};
obj = (cpy.PyBytes_FromStringAndSize)(ptr, size_v as isize);
}
8 => {
if size_v != 8 {
return false;
}
let mut i = [0u8; 4];
i.copy_from_slice(&val_payload[4..8]);
let inst_id = u32::from_le_bytes(i);
if let Some(p) = OBJ_PTRS.lock().unwrap().get(&inst_id).copied() {
(cpy.Py_IncRef)(p.0);
obj = p.0;
} else {
return false;
}
}
_ => return false,
}
if obj.is_null() {
return false;
}
let rc = (cpy.PyDict_SetItemString)(dict, key_c.as_ptr(), obj);
(cpy.Py_DecRef)(obj);
if rc != 0 {
return false;
}
}
}
true
}
fn should_autodecode() -> bool {
std::env::var("NYASH_PY_AUTODECODE")
.map(|v| v != "0")
.unwrap_or(false)
}
fn try_write_autodecode(
cpy: &CPython,
obj: *mut PyObject,
result: *mut u8,
result_len: *mut usize,
) -> bool {
unsafe {
// Float -> tag=5
let mut had_err = false;
let f = (cpy.PyFloat_AsDouble)(obj);
if !(cpy.PyErr_Occurred)().is_null() {
had_err = true;
(cpy.PyErr_Clear)();
}
if !had_err {
eprintln!("[PyPlugin] autodecode: Float {}", f);
let mut payload = [0u8; 8];
payload.copy_from_slice(&f.to_le_bytes());
let rc = write_tlv_result(&[(5u8, &payload)], result, result_len);
return rc == NYB_SUCCESS || rc == NYB_E_SHORT_BUFFER;
}
// Integer (PyLong) -> tag=3 (i64)
let i = (cpy.PyLong_AsLongLong)(obj);
if !(cpy.PyErr_Occurred)().is_null() {
(cpy.PyErr_Clear)();
} else {
eprintln!("[PyPlugin] autodecode: I64 {}", i);
let mut payload = [0u8; 8];
payload.copy_from_slice(&i.to_le_bytes());
let rc = write_tlv_result(&[(3u8, &payload)], result, result_len);
return rc == NYB_SUCCESS || rc == NYB_E_SHORT_BUFFER;
}
// Unicode -> tag=6
let u = (cpy.PyUnicode_AsUTF8)(obj);
if !(cpy.PyErr_Occurred)().is_null() {
(cpy.PyErr_Clear)();
} else if !u.is_null() {
let s = CStr::from_ptr(u).to_string_lossy();
eprintln!("[PyPlugin] autodecode: String '{}', len={} ", s, s.len());
let rc = write_tlv_result(&[(6u8, s.as_bytes())], result, result_len);
return rc == NYB_SUCCESS || rc == NYB_E_SHORT_BUFFER;
}
// Bytes -> tag=7
let mut ptr: *mut c_char = std::ptr::null_mut();
let mut sz: isize = 0;
if (cpy.PyBytes_AsStringAndSize)(obj, &mut ptr, &mut sz) == 0 {
let slice = std::slice::from_raw_parts(ptr as *const u8, sz as usize);
eprintln!("[PyPlugin] autodecode: Bytes {} bytes", sz);
let rc = write_tlv_result(&[(7u8, slice)], result, result_len);
return rc == NYB_SUCCESS || rc == NYB_E_SHORT_BUFFER;
} else if !(cpy.PyErr_Occurred)().is_null() {
(cpy.PyErr_Clear)();
}
false
}
}