diff --git a/setup.py b/setup.py index f0d202fc..ff464a37 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,8 @@ SANITIZE = os.environ.get("MSGSPEC_SANITIZE", False) COVERAGE = os.environ.get("MSGSPEC_COVERAGE", False) DEBUG = os.environ.get("MSGSPEC_DEBUG", SANITIZE or COVERAGE) +TEST_CAPI = os.environ.get("MSGSPEC_COMPILE_TEST_CAPI", True) + extra_compile_args = [] extra_link_args = [] @@ -61,6 +63,18 @@ ) ] +# Ignored by default but needed whenever needing to test msgspec's CAPI Capsule During Lower-level usage... +if TEST_CAPI: + ext_modules.append( + Extension( + "msgspec._testcapi", + [os.path.join("src", "msgspec", "_testcapi.c")], + extra_compile_args=extra_compile_args, + extra_link_args=extra_link_args, + ) + ) + + setup( ext_modules=ext_modules, ) diff --git a/src/msgspec/_core.c b/src/msgspec/_core.c index 320ebca7..33f22069 100644 --- a/src/msgspec/_core.c +++ b/src/msgspec/_core.c @@ -16,6 +16,9 @@ #include "ryu.h" #include "atof.h" +/* C-API */ +#include "msgspec.h" + /* Python version checks */ #define PY311_PLUS (PY_VERSION_HEX >= 0x030b0000) #define PY312_PLUS (PY_VERSION_HEX >= 0x030c0000) @@ -2444,6 +2447,24 @@ static PyTypeObject Factory_Type = { .tp_members = Factory_members, }; +/************************************************************************* + * Factory C-API * + *************************************************************************/ + +/* Factory_New is already implemented so no need to put it here we just have +to remeber to initalize it */ + +PyObject* Factory_Create(PyObject* self){ + if (!Py_IS_TYPE(self, &Factory_Type)){ + PyErr_Format(PyExc_TypeError, "expected a msgspec.Factory type got %R", self); + return NULL; + } + /* Call lower level now that typecheck has been properly handled... */ + return Factory_Call(self); +} + + + /************************************************************************* * Field * *************************************************************************/ @@ -2574,6 +2595,79 @@ static PyTypeObject Field_Type = { .tp_members = Field_members, }; +/************************************************************************* + * Field C-API * + *************************************************************************/ + +int Field_CheckOrFail(PyObject* self){ + if (!Py_IS_TYPE(self, &Field_Type)){ + PyErr_SetString( + PyExc_TypeError, "field must be a msgspec field type" + ); + return -1; + } + return 0; +} + +/* same as Field_new although much faster due to not needing argument parsing... */ +PyObject* Field_New(PyObject* name, PyObject* value, PyObject* factory){ + if (name == Py_None) { + name = NULL; + } + else if (!PyUnicode_CheckExact(name)) { + PyErr_SetString(PyExc_TypeError, "name must be a str or None"); + return NULL; + } + if ((value != NODEFAULT && value != NULL) && (factory != NODEFAULT && factory != NULL)) { + PyErr_SetString( + PyExc_TypeError, "Cannot set both `value` and `factory`" + ); + return NULL; + } + if (factory != NODEFAULT && factory != NULL) { + if (!PyCallable_Check(factory)) { + PyErr_SetString(PyExc_TypeError, "factory must be callable"); + return NULL; + } + } + Field *self = (Field *)Field_Type.tp_alloc(&Field_Type, 0); + if (self == NULL) return NULL; + self->default_value = (value != NULL) ? Py_NewRef(value) : Py_NewRef(NODEFAULT); + self->default_factory = (factory != NULL) ? Py_NewRef(factory) : Py_NewRef(NODEFAULT); + Py_XINCREF(name); + self->name = name; + return (PyObject *)self; +} + +int Field_GetName(PyObject* self, PyObject** name){ + if (Field_CheckOrFail(self) < 0){ + *name = NULL; + return -1; + } + *name = Py_NewRef(((Field*)self)->name); + return 0; +} + + +int Field_GetDefault(PyObject* self, PyObject** value){ + if (Field_CheckOrFail(self) < 0){ + *value = NULL; + return -1; + } + *value = Py_NewRef(((Field*)self)->default_value); + return 0; +} + + +int Field_GetFactory(PyObject* self, PyObject** factory){ + if (Field_CheckOrFail(self) < 0){ + *factory = NULL; + return -1; + } + *factory = Py_NewRef(((Field*)self)->default_factory); + return 0; +} + /************************************************************************* * AssocList & order handling * *************************************************************************/ @@ -2774,6 +2868,8 @@ AssocList_Sort(AssocList* list) { * Struct, PathNode, and TypeNode Types * *************************************************************************/ +/* TODO: Vizonex I might consider putting this in the C-API as enums or in some other way... */ + /* Types */ #define MS_TYPE_ANY (1ull << 0) #define MS_TYPE_NONE (1ull << 1) @@ -5658,7 +5754,9 @@ structmeta_get_module_ns(MsgspecState *mod, StructMetaInfo *info) { static int structmeta_collect_base(StructMetaInfo *info, MsgspecState *mod, PyObject *base) { - if ((PyTypeObject *)base == &StructMixinType) return 0; + /* StructMixin could be theoretically subclassed for other + purpouses see: _testcapi.c */ + if (Py_IS_TYPE(base, &StructMixinType)) return 0; if (((PyTypeObject *)base)->tp_weaklistoffset) { info->already_has_weakref = true; @@ -6657,6 +6755,84 @@ StructMeta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) ); } +/************************************************************************* + * StructMeta C-API * + *************************************************************************/ + +static PyObject* +_StructMeta_New( + PyTypeObject *type, + PyObject *name, + PyObject *bases, + PyObject *namespace, + PyObject *arg_tag_field, + PyObject *arg_tag, + PyObject *arg_rename, + int arg_omit_defaults, + int arg_forbid_unknown_fields, + int arg_frozen, + int arg_eq, + int arg_order, + bool arg_kw_only, + int arg_repr_omit_defaults, + int arg_array_like, + int arg_gc, + int arg_weakref, + int arg_dict, + int arg_cache_hash +){ + if (type == NULL || !ms_is_struct_meta(type)){ + PyErr_SetString(PyExc_TypeError, "`type` must inherit from `StructMeta`"); + return NULL; + } + + if (name == NULL || !PyUnicode_Check(name)){ + PyErr_SetString(PyExc_TypeError, "`name` must be a `str` type"); + return NULL; + } + + if (bases == NULL || !PyTuple_Check(bases)){ + PyErr_SetString(PyExc_TypeError, "`bases` must be a `tuple` type"); + return NULL; + } + + if (namespace == NULL || !PyDict_Check(namespace)){ + PyErr_SetString(PyExc_TypeError, "`namespace` must be a `dict` type"); + return NULL; + } + + return StructMeta_new_inner( + type, name, bases, namespace, + arg_tag_field, arg_tag, arg_rename, + arg_omit_defaults, arg_forbid_unknown_fields, + arg_frozen, arg_eq, arg_order, arg_kw_only, + arg_repr_omit_defaults, arg_array_like, + arg_gc, arg_weakref, arg_dict, arg_cache_hash + ); + +} + +static inline int StructMeta_CheckOrFail(PyObject* self){ + if (!ms_is_struct_inst(self)){ + PyErr_Format(PyExc_TypeError, "`self` must inherit from `StructMeta` but `%s` doesn't", Py_TYPE(self)->tp_name); + return -1; + } + return 0; +} + +static PyObject* StructMeta_GetFieldName( + PyObject* self, + Py_ssize_t index +){ + return (StructMeta_CheckOrFail(self) < 0) ? NULL: StructMeta_get_field_name(self, index); +} + + + + +/************************************************************************* + * defstruct * + *************************************************************************/ PyDoc_STRVAR(msgspec_defstruct__doc__, "defstruct(name, fields, *, bases=None, module=None, namespace=None, " @@ -9215,6 +9391,67 @@ static PyTypeObject Ext_Type = { .tp_methods = Ext_methods }; + +/************************************************************************* + * Ext C-API * + *************************************************************************/ + +static int _Ext_TypeCheck_Or_Fail(PyObject* self){ + if (!Py_IS_TYPE(self, &Ext_Type)){ + PyErr_Format( + PyExc_TypeError, + "self must be an Ext type got %.200s", + Py_TYPE(self)->tp_name + ); + return -1; + } + return 0; +} + +static PyObject* +_Ext_New(long code, PyObject* data){ + if (code > 127 || code < -128) { + PyErr_SetString( + PyExc_ValueError, + "code must be an int between -128 and 127" + ); + return NULL; + } + if (!(PyBytes_CheckExact(data) || PyByteArray_CheckExact(data) || PyObject_CheckBuffer(data))) { + PyErr_Format( + PyExc_TypeError, + "data must be a bytes, bytearray, or buffer-like object, got %.200s", + Py_TYPE(data)->tp_name + ); + return NULL; + } + return Ext_New(code, data); +} + +/* Returns data from extension type, Returns -1 if Type is not an Ext type */ +static int Ext_GetData(PyObject* self, PyObject** data){ + if (_Ext_TypeCheck_Or_Fail(self) < 0){ + data = NULL; + return -1; + } + *data = Py_NewRef(((Ext*)self)->data); + return 0; +}; + +/* Returns code from extension type, Returns -1 if Type is not an Ext type */ +static int Ext_GetCode(PyObject* self, long* code){ + if (_Ext_TypeCheck_Or_Fail(self) < 0){ + code = NULL; + return -1; + } + *code = ((Ext*)self)->code; + return 0; +} + + + + + /************************************************************************* * Dataclass Utilities * *************************************************************************/ @@ -22170,6 +22407,77 @@ msgspec_convert(PyObject *self, PyObject *args, PyObject *kwargs) } +/************************************************************************* + * C-API Capsule Setup * + *************************************************************************/ + + +/* Also needed to get the CAPI to work properly */ + +// gh-106307 added PyModule_Add() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyModule_Add(PyObject *mod, const char *name, PyObject *value) +{ + int res = PyModule_AddObjectRef(mod, name, value); + Py_XDECREF(value); + return res; +} +#endif + + +static void +capsule_free(Msgspec_CAPI* capi) +{ + PyMem_Free(capi); +} + +static void +capsule_destructor(PyObject* o) +{ + Msgspec_CAPI* capi = PyCapsule_GetPointer(o, MSGSPEC_CAPI_NAME); + capsule_free(capi); +} + +static PyObject* +new_capsule(MsgspecState* state){ + Msgspec_CAPI* capi = + (Msgspec_CAPI*)PyMem_Malloc(sizeof(Msgspec_CAPI)); + if (capi == NULL) { + PyErr_NoMemory(); + return NULL; + } + + /* TYPES */ + capi->Ext_Type = &Ext_Type; + capi->Factory_Type = &Factory_Type; + capi->Field_Type = &Field_Type; + capi->StructMeta_Type = &StructMetaType; + capi->StructMixin_Type = &StructMixinType; + + capi->Ext_New = _Ext_New; + capi->Ext_GetCode = Ext_GetCode; + capi->Ext_GetData = Ext_GetData; + + capi->Factory_New = Factory_New; + capi->Factory_Create = Factory_Create; + + capi->Field_New = Field_New; + capi->Field_GetDefault = Field_GetDefault; + capi->Field_GetFactory = Field_GetFactory; + + capi->StructMeta_New = _StructMeta_New; + capi->StructMeta_GetFieldName = StructMeta_GetFieldName; + + PyObject* ret = + PyCapsule_New(capi, MSGSPEC_CAPSULE_NAME, capsule_destructor); + if (ret == NULL) { + capsule_free(capi); + } + return ret; +} + + /************************************************************************* * Module Setup * *************************************************************************/ @@ -22701,6 +23009,15 @@ PyInit__core(void) if (st->StructType == NULL) return NULL; Py_INCREF(st->StructType); if (PyModule_AddObject(m, "Struct", st->StructType) < 0) return NULL; + + PyObject *capsule = new_capsule(st); + if (capsule == NULL) { + return NULL; + } + if (PyModule_Add(m, MSGSPEC_CAPI_NAME, capsule) < 0) { + return NULL; + } + #ifdef Py_GIL_DISABLED PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif diff --git a/src/msgspec/_testcapi.c b/src/msgspec/_testcapi.c new file mode 100644 index 00000000..43942039 --- /dev/null +++ b/src/msgspec/_testcapi.c @@ -0,0 +1,375 @@ +#define PY_SSIZE_T_CLEAN +#include +#define MSGSPEC_USE_STRUCTURES +#define MSGSPEC_USE_CAPSULE_API +#include "msgspec.h" + +#include + +/* Modeled after multidict's C-API tests written by the same author Vizonex */ + +/* TODO: (Vizonex) + I think I could make a template github repo + for users who want a quick and dirty template to work off of for + something like this... +*/ + +typedef struct _mod_state { + Msgspec_CAPI* capi; +} mod_state; + +static PyModuleDef _testcapi_module; + +static inline mod_state * +get_mod_state(PyObject *mod) +{ + mod_state *state = (mod_state *)PyModule_GetState(mod); + assert(state != NULL); + return state; +} + +static mod_state * +testcapi_get_global_state(void) +{ + PyObject *module = PyState_FindModule(&_testcapi_module); + return module == NULL ? NULL : get_mod_state(module); +} + +static inline Msgspec_CAPI* +get_capi(PyObject *mod) +{ + return get_mod_state(mod)->capi; +} + +static int +check_nargs(const char *name, const Py_ssize_t nargs, const Py_ssize_t required) +{ + if (nargs != required) { + PyErr_Format(PyExc_TypeError, + "%s should be called with %d arguments, got %d", + name, + required, + nargs); + return -1; + } + return 0; +} + +/* Factory Objects */ +static PyObject * factory_type(PyObject* mod, PyObject* Py_UNUSED(unused)){ + return Py_NewRef((PyObject*)get_capi(mod)->Factory_Type); +} + +// PyObject *const *args, Py_ssize_t nargs + +static PyObject* factory_check(PyObject* mod, PyObject* arg){ + if (Factory_Check(get_capi(mod), arg)){ + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + +// TBD +// static PyObject* factory_check_exact(PyObject* mod, PyObject* arg){ +// if (Factory_CheckExact(get_capi(mod), arg)){ +// Py_RETURN_TRUE; +// } +// Py_RETURN_FALSE; +// } + +static PyObject* factory_new(PyObject* mod, PyObject* arg){ + return get_capi(mod)->Factory_New(arg); +} + +static PyObject* factory_create(PyObject* mod, PyObject* arg){ + return get_capi(mod)->Factory_Create(arg); +} + + +/* Field_Type Objects... */ + +static PyObject* field_check(PyObject* mod, PyObject* arg){ + if (Field_Check(get_capi(mod), arg)){ + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + +static PyObject * field_type(PyObject* mod, PyObject* Py_UNUSED(unused)){ + return Py_NewRef((PyObject*)get_capi(mod)->Field_Type); +} + +static PyObject* field_new(PyObject* mod, PyObject *const *args, Py_ssize_t nargs){ + if (check_nargs("field_new", nargs, 3) < 0){ + return NULL; + } + // Will simulate None as NULL since it's kind of difficult to simulate a Null Pointer... + PyObject* _default = Py_IsNone(args[1]) ? NULL: Py_NewRef(args[1]); + PyObject* _factory = Py_IsNone(args[2]) ? NULL: Py_NewRef(args[2]); + PyObject* ret = get_capi(mod)->Field_New(args[0], _default, _factory); + /* Py_CLEAR has null checks of it's own before it clears out a value... */ + Py_CLEAR(_default); + Py_CLEAR(_factory); + return ret; +} + +typedef int (*capi_getter_func)(PyObject* self, PyObject** value); + +static PyObject* handle_getter_capi_func(capi_getter_func func, PyObject* self){ + PyObject* value = NULL; + if (func(self, &value) < 0){ + return NULL; + } + return value; +} + +static PyObject* handle_getter_capi_func_can_be_null(capi_getter_func func, PyObject* self){ + PyObject* value = NULL; + if (func(self, &value) < 0){ + return NULL; + } + if (value == NULL){ + Py_RETURN_NONE; + } + return value; +} + + +static PyObject* field_get_name(PyObject* mod, PyObject* arg){ + return handle_getter_capi_func(get_capi(mod)->Field_GetName, arg); +} +static PyObject* field_get_default(PyObject* mod, PyObject* arg){ + return handle_getter_capi_func_can_be_null(get_capi(mod)->Field_GetDefault, arg); +} +static PyObject* field_get_factory(PyObject* mod, PyObject* arg){ + return handle_getter_capi_func_can_be_null(get_capi(mod)->Field_GetFactory, arg); +} + + +/* This test simulates something very close to that of SQLModel +and it simulates an ORM styled library where __table_name__ & __abstract_table__ attributes +are added in without disrupting Struct or anything that comes after it... */ + +typedef struct { + StructMetaObject base; + /* These items should not be called with other fellow struct + memebers these are primarly used as class attributes */ + const char* table_name; + int abstract_table; +} TableMetaObject; + +static PyTypeObject TableMetaType; + +/* tp_base is a lifesaver and is the key to making a working subclassable type */ + +static PyObject* TableMeta_New(PyTypeObject* type, PyObject* args, PyObject* kwargs){ + /* Will access the newly made object using PyType_GenericNew to save us a hassle */ + int abstract = 1; + int grab_later = 0; + const char* table_name = NULL; + + mod_state* state = testcapi_get_global_state(); + PyObject* table = NULL; + + /* kwargs can show up as NULL whenever it feels like it so we need an extra check here. */ + if (kwargs != NULL) + table = PyDict_GetItemString(kwargs, "table"); + if (table != NULL){ + /* table is confirmed now so we + initalize that value to prevent it + from getting deleted */ + Py_INCREF(table); + + if (PyUnicode_Check(table)){ + table_name = PyUnicode_AsUTF8(table); + abstract = 0; + } else if (PyBool_Check(table)) { + if (Py_IsTrue(table)){ + abstract = 0; + /* We grab the name from the class object being derrived instead*/ + table_name = PyUnicode_AsUTF8(PyTuple_GET_ITEM(args, 0)); + } + /* if flase we shall remain abstract */ + } + /* + disallow msgspec from complaining about our + newly added attribute by deleting it... + */ + PyDict_DelItemString(kwargs, "table"); + } + TableMetaObject *obj = (TableMetaObject*)type->tp_base->tp_new(type, args, kwargs); + if (obj == NULL) + return NULL; + obj->abstract_table = abstract; + obj->table_name = table_name; + return (PyObject*)obj; +} + + +static int table_meta_traverse(PyObject * self, visitproc visit, void * arg){ + return Py_TYPE(self)->tp_base->tp_traverse((PyObject*)self, visit, arg); +} + +static int table_meta_clear(PyObject * self){ + return Py_TYPE(self)->tp_base->tp_clear((PyObject*)self); +} + +static void table_meta_dealloc(PyObject* self){ + Py_TYPE(self)->tp_base->tp_dealloc((PyObject*)self); +} + +static PyObject* TableMixin_table_name(PyObject* self, void* closure){ + TableMetaObject* meta = ((TableMetaObject*)Py_TYPE(self)); + if (meta->abstract_table) + Py_RETURN_NONE; + return PyUnicode_FromString(meta->table_name); +}; + +static PyObject* TableMixin_abstract_table(PyObject* self, void* closure){ + return PyBool_FromLong(((TableMetaObject*)Py_TYPE(self))->abstract_table); +} + +static PyGetSetDef TableMixin_getset[] = { + {"__table_name__", (getter)TableMixin_table_name, NULL, "Table name", NULL}, + {"__abstract_table__", (getter)TableMixin_abstract_table, NULL, "flag for checking if given ORM Table is abstract", NULL}, + {NULL}, +}; + +static PyTypeObject TableMetaType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "msgspec._testcapi.TableMeta", + .tp_basicsize = sizeof(TableMetaObject), + .tp_itemsize = 0, + .tp_vectorcall_offset = offsetof(PyTypeObject, tp_vectorcall), + .tp_call = PyVectorcall_Call, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_TYPE_SUBCLASS | Py_TPFLAGS_HAVE_GC | _Py_TPFLAGS_HAVE_VECTORCALL | Py_TPFLAGS_BASETYPE, + .tp_new = TableMeta_New, + .tp_traverse = (traverseproc)table_meta_traverse, + .tp_clear = (inquiry)table_meta_clear, + .tp_dealloc = (destructor)table_meta_dealloc, +}; + +/* This MixinType will allow ourselves gain access to our class attributes +without screwing around with Msgspec's internal types */ +static PyTypeObject TableMixinType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "msgspec._testcapi.TableMixin", + .tp_basicsize = 0, + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_getset = TableMixin_getset, +}; + + + +/* module slots */ + +static int +module_traverse(PyObject *mod, visitproc visit, void *arg) +{ + return 0; +} + +static int +module_clear(PyObject *mod) +{ + return 0; +} + +static void +module_free(void *mod) +{ + (void)module_clear((PyObject *)mod); +} + +/* These can get annoying to configure so I made simple macros - Vizonex */ +#define MM_O(name) \ + {#name, (PyCFunction)(name), METH_O} + +#define MM_NOARGS(name) \ + {#name, (PyCFunction)(name), METH_NOARGS} + +#define MM_FASTCALL(name) \ + {#name, (PyCFunction)(name), METH_FASTCALL} + + +static PyMethodDef module_methods[] = { + MM_O(factory_check), + MM_O(factory_new), + MM_O(factory_create), + MM_NOARGS(factory_type), + MM_O(field_check), + MM_O(field_get_default), + MM_O(field_get_factory), + MM_O(field_get_name), + MM_FASTCALL(field_new), + MM_NOARGS(field_type), + {NULL, NULL} +}; + +PyDoc_STRVAR(Table__doc__, +"A Subclass of StructMeta meant to give an example of how msgspec could be used as an ORM Library." +); + +static int +module_exec(PyObject *mod) +{ + mod_state *state = get_mod_state(mod); + state->capi = Msgspec_Import(); + if (state->capi == NULL) { + return -1; + } + + TableMetaType.tp_base = state->capi->StructMeta_Type; + TableMixinType.tp_base = state->capi->StructMixin_Type; + + if (PyType_Ready(&TableMetaType) < 0) + return -1; + + if (PyModule_AddObjectRef(mod, "TableMeta", (PyObject*)(&TableMetaType)) < 0) + return -1; + + if (PyType_Ready(&TableMixinType) < 0) + return -1; + + if (PyModule_AddObjectRef(mod, "TableMixin", (PyObject*)(&TableMixinType)) < 0) + return -1; + + PyObject* TableType = PyObject_CallFunction( + (PyObject *)&TableMetaType, "s(O){ssss}", "Table", &TableMixinType, + "__module__", "msgspec._testcapi", "__doc__", Table__doc__ + ); + if (TableType == NULL) return -1; + Py_INCREF(TableType); + if (PyModule_AddObject(mod, "Table", TableType) < 0) + return -1; + return 0; +} + +static struct PyModuleDef_Slot module_slots[] = { + {Py_mod_exec, module_exec}, +#if PY_VERSION_HEX >= 0x030c00f0 + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, +#endif +#if PY_VERSION_HEX >= 0x030d00f0 + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL}, +}; + +static PyModuleDef _testcapi_module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "msgspec._testcapi", + .m_size = sizeof(mod_state), + .m_methods = module_methods, + .m_slots = module_slots, + .m_traverse = module_traverse, + .m_clear = module_clear, + .m_free = (freefunc)module_free, +}; + +PyMODINIT_FUNC +PyInit__testcapi(void) +{ + return PyModuleDef_Init(&_testcapi_module); +} diff --git a/src/msgspec/msgspec.h b/src/msgspec/msgspec.h new file mode 100644 index 00000000..f193e542 --- /dev/null +++ b/src/msgspec/msgspec.h @@ -0,0 +1,302 @@ +#ifndef MSGSPEC_H +#define MSGSPEC_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This code is inspired by multidict's concept for a C-API Capsule written by Vizonex and Avestlov +* Written by Vizonex */ + +#define MSGSPEC_MODULE_NAME "msgspec._core" +#define MSGSPEC_CAPI_NAME "CAPI" +#define MSGSPEC_CAPSULE_NAME MSGSPEC_MODULE_NAME "." MSGSPEC_CAPI_NAME + + +/* TODO consider Moving EncoderState over to another header file and let it have some iteraction here... */ +typedef struct EncoderState EncoderState; + + + + +typedef struct _msgspec_capi { + /* module_state (private) */ + void* _state; + + PyTypeObject* Ext_Type; + PyTypeObject* Factory_Type; + PyTypeObject* Field_Type; + + // Coming Soon... + // PyTypeObject* Raw_Type; + + /* used for internally subclassing within C */ + PyTypeObject* StructMeta_Type; + PyTypeObject* StructMixin_Type; + + /* Kept things in alphabetical order for the sake of neatness + let me know if this is not the order you want these in... */ + + /* EXT */ + + /* Creates a new Extension Type, returns NULL if it fails, + this will raise a ValueError if code + is not between -128 and 127 and TypeError + if data is not a bytes or bytearray object */ + PyObject * (*Ext_New)(long code, PyObject* data); + + /* Returns data from extension type, Returns -1 if Type is not an Ext type */ + int (*Ext_GetData)(PyObject* self, PyObject** data); + + /* Returns code from extension type, Returns -1 if Type is not an Ext type */ + int (*Ext_GetCode)(PyObject* self, long* code); + + + + /* Factory */ + + /* Creates a new object from factory Type, returns NULL on exception*/ + PyObject* (*Factory_New)(PyObject* factory); + PyObject* (*Factory_Create)(PyObject* self); + + + /* Fields */ + + PyObject* (*Field_New)(PyObject* name, PyObject* value, PyObject* factory); + + /* In Cython there is no need for these due to safely having checks of it's own to grab attributes + however, in other projects (CPython or Rust) this it may be nessesary to have here... */ + + /* Gets the name of the field, returns -1 if type is not a msgspec field + Field name can be NULL if it wasn't set by a msgspec structure yet... */ + int (*Field_GetName)(PyObject* self, PyObject** name); + + /* Obtains default attribute, + returns -1 if type is not a msgspec field value could appear as NULL if field was a NODEFAULT */ + int (*Field_GetDefault)(PyObject* self, PyObject** value); + + /* Obtains factory attribute, returns -1 if type is not a msgspec field */ + int (*Field_GetFactory)(PyObject* self, PyObject** factory); + + /* Vizonex Note: (I'm deleting this note when the PR is done...) + Although rather big some developers have asked to subclass StructMeta + And with cython being unable to implement metaclasses in a feasable way + to do what msgspec does it made more sense to me to just shove it in here + + I have need to subclass this object for when I go to make a new library + called specsql which will have a TableMeta class for adding a tablename but + also to ensure that it has an attribute __tablename__ or __table__ to identify + itself. + + SQLAlchemy and SQLModel are both slow and do not have the same serlization capabilities + and performace speeds that this library can achieve to begin with, hence my desire to + add it. + */ + + /* Creates a new StructMeta structure, this can also be used to help subclass + StructMeta for other lower-level projects that need StructMeta for Structure + creation */ + PyObject* (*StructMeta_New)( + PyTypeObject *type, + PyObject *name, + PyObject *bases, + PyObject *namespace, + PyObject *arg_tag_field, + PyObject *arg_tag, + PyObject *arg_rename, + int arg_omit_defaults, + int arg_forbid_unknown_fields, + int arg_frozen, + int arg_eq, + int arg_order, + bool arg_kw_only, + int arg_repr_omit_defaults, + int arg_array_like, + int arg_gc, + int arg_weakref, + int arg_dict, + int arg_cache_hash + ); + + /* Obtains field information good for debugging or other use-cases + Returns NULL and raises TypeError if self is not inherited from + StructMeta */ + PyObject* (*StructMeta_GetFieldName)(PyObject* self, Py_ssize_t index); + +} Msgspec_CAPI; + +/* Capsule */ + +/* imports msgspec capsule +returns NULL if import fails. */ +static inline Msgspec_CAPI* +Msgspec_Import() +{ + return (Msgspec_CAPI*)PyCapsule_Import(MSGSPEC_CAPSULE_NAME, 0); +} + +#ifdef MSGSPEC_USE_CAPSULE_API // Define if your not using Cython or want to use your own capsule + + +/************************************************************************* + * Ext * + *************************************************************************/ + + + + +/************************************************************************* + * Factory * + *************************************************************************/ + +static inline int Factory_Check(Msgspec_CAPI* api, PyObject* ob){ + return Py_IS_TYPE(ob, api->Factory_Type); +} + +static inline int Factory_CheckExact(Msgspec_CAPI* api, PyObject* ob){ + return Py_IS_TYPE(ob, api->Factory_Type) || PyObject_TypeCheck(ob, api->Factory_Type); +} + +/************************************************************************* + * Field * + *************************************************************************/ + +static inline int Field_Check(Msgspec_CAPI* api, PyObject* ob){ + return Py_IS_TYPE(ob, api->Field_Type); +} + +static inline int Field_CheckExact(Msgspec_CAPI* api, PyObject* ob){ + return Py_IS_TYPE(ob, api->Field_Type) || PyObject_TypeCheck(ob, api->Field_Type); +} + + +/************************************************************************* + * StructMeta * + *************************************************************************/ + +static inline int StructMeta_Check(Msgspec_CAPI* api, PyObject* ob){ + return Py_IS_TYPE(ob, api->StructMeta_Type); +} + +static inline int StructMeta_CheckExact(Msgspec_CAPI* api, PyObject* ob){ + return Py_IS_TYPE(ob, api->StructMeta_Type) || PyObject_TypeCheck(ob, api->StructMeta_Type); +} + + + +#endif /* MSGSPEC_USE_CAPSULE_API */ + + +#ifdef MSGSPEC_USE_STRUCTURES + +typedef union TypeDetail { + int64_t i64; + double f64; + Py_ssize_t py_ssize_t; + void *pointer; +} TypeDetail; + +typedef struct TypeNode { + uint64_t types; + TypeDetail details[]; +} TypeNode; + +/* A simple extension of TypeNode to allow for static allocation */ +typedef struct { + uint64_t types; + TypeDetail details[1]; +} TypeNodeSimple; + +typedef struct { + PyObject_HEAD + PyObject *int_lookup; + PyObject *str_lookup; + bool literal_none; +} LiteralInfo; + +typedef struct { + PyObject *key; + TypeNode *type; +} TypedDictField; + +typedef struct { + PyObject_VAR_HEAD + Py_ssize_t nrequired; + TypedDictField fields[]; +} TypedDictInfo; + +typedef struct { + PyObject *key; + TypeNode *type; +} DataclassField; + +typedef struct { + PyObject_VAR_HEAD + PyObject *class; + PyObject *pre_init; + PyObject *post_init; + PyObject *defaults; + DataclassField fields[]; +} DataclassInfo; + +typedef struct { + PyObject_VAR_HEAD + PyObject *class; + PyObject *defaults; + TypeNode *types[]; +} NamedTupleInfo; + +struct StructInfo; + +typedef struct { + PyHeapTypeObject base; + PyObject *struct_fields; + PyObject *struct_defaults; + Py_ssize_t *struct_offsets; + PyObject *struct_encode_fields; + struct StructInfo *struct_info; + Py_ssize_t nkwonly; + Py_ssize_t n_trailing_defaults; + PyObject *struct_tag_field; /* str or NULL */ + PyObject *struct_tag_value; /* str or NULL */ + PyObject *struct_tag; /* True, str, or NULL */ + PyObject *match_args; + PyObject *rename; + PyObject *post_init; + Py_ssize_t hash_offset; /* 0 for no caching, otherwise offset */ + int8_t frozen; + int8_t order; + int8_t eq; + int8_t repr_omit_defaults; + int8_t array_like; + int8_t gc; + int8_t omit_defaults; + int8_t forbid_unknown_fields; +} StructMetaObject; + +typedef struct StructInfo { + PyObject_VAR_HEAD + StructMetaObject *class; +#ifdef Py_GIL_DISABLED + _Atomic(uint8_t) initialized; +#endif + TypeNode *types[]; +} StructInfo; + +typedef struct { + PyObject_HEAD + StructMetaObject *st_type; +} StructConfig; + +#endif /* MSGSPEC_USE_STRUCTURES */ + + +#ifdef __cplusplus +} +#endif + +#endif // MSGSPEC_H \ No newline at end of file diff --git a/tests/unit/test_capi.py b/tests/unit/test_capi.py new file mode 100644 index 00000000..644ea02e --- /dev/null +++ b/tests/unit/test_capi.py @@ -0,0 +1,93 @@ +import pytest +from msgspec._core import Factory, Field # noqa: F401 + +from msgspec import NODEFAULT, field + +_testcapi = pytest.importorskip("msgspec._testcapi") + + +def test_factory_type(): + t = _testcapi.factory_type() + assert str(t) == "" + + +def test_factory_check(): + f = Factory(str) + assert _testcapi.factory_check(f) + not_a_factory = "NOT IT" + assert not _testcapi.factory_check(not_a_factory) + + +def test_factory_new(): + f = _testcapi.factory_new(str) + assert isinstance(f, Factory) + + # Should raise if item is not callable... + with pytest.raises(TypeError): + _testcapi.factory_new("NOT CALLABLE") + + +def test_factory_create(): + f = Factory(list) + assert _testcapi.factory_create(f) == [] + + +def test_field_type(): + t = _testcapi.field_type() + # signature should match + assert str(t) == "" + + +def test_field_check(): + f = field(default=1) + assert _testcapi.field_check(f) + assert not _testcapi.field_check("NOT A FIELD") + + +def test_field_new(): + # None is used to simulate NULL to test a default will use something else... + f = _testcapi.field_new("name", None, None) + assert isinstance(f, Field) + # Now Simulate with NoDefault + f = _testcapi.field_new("name", NODEFAULT, NODEFAULT) + assert isinstance(f, Field) + + # Try simultation of Default as a string + f = _testcapi.field_new("name", "default", NODEFAULT) + assert isinstance(f, Field) + + # Simulate not being able to intake both a default and factory + with pytest.raises(TypeError, match="Cannot set both `value` and `factory`"): + _testcapi.field_new("name", 0, 0) + + with pytest.raises(TypeError, match="factory must be callable"): + _testcapi.field_new("name", NODEFAULT, 0) + + +def test_field_get_default(): + f = field(default="X") + assert _testcapi.field_get_default(f) == "X" + + +def test_subclassed_Struct_with_attributes(): + class XYTableBase(_testcapi.Table): + """ + Base Table used to simulate a ORM Program + """ + + class XYTable(_testcapi.Table, table=True): + x:int + y:int + + x = XYTable(3, 1) + assert x.x == 3 + assert x.y == 1 + assert x.__table_name__ == "XYTable" + + + +# TODO (Vizonex) +# - field_get_default (Exception Case) +# - field_get_facotry +# - field_get_name +# - field_new (Exception Case of trying both Factory and Default)