Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 196 additions & 1 deletion c_src/py_callback.c
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,168 @@ PyTypeObject ErlangPidType = {
.tp_doc = "Opaque Erlang process identifier",
};

/* ============================================================================
* ErlangRef - opaque Erlang reference type
*
* Stores the reference as a serialized binary for round-trip through Python.
* ============================================================================ */

static void ErlangRef_dealloc(ErlangRefObject *self) {
if (self->data != NULL) {
PyMem_Free(self->data);
}
Py_TYPE(self)->tp_free((PyObject *)self);
}

static PyObject *ErlangRef_repr(ErlangRefObject *self) {
return PyUnicode_FromFormat("<erlang.Ref size=%zu>", self->size);
}

static PyObject *ErlangRef_richcompare(PyObject *a, PyObject *b, int op) {
if (!Py_IS_TYPE(b, &ErlangRefType)) {
Py_RETURN_NOTIMPLEMENTED;
}
ErlangRefObject *ra = (ErlangRefObject *)a;
ErlangRefObject *rb = (ErlangRefObject *)b;

int cmp = 0;
if (ra->size != rb->size) {
cmp = (ra->size < rb->size) ? -1 : 1;
} else {
cmp = memcmp(ra->data, rb->data, ra->size);
}

switch (op) {
case Py_EQ: return PyBool_FromLong(cmp == 0);
case Py_NE: return PyBool_FromLong(cmp != 0);
default: Py_RETURN_NOTIMPLEMENTED;
}
}

static Py_hash_t ErlangRef_hash(ErlangRefObject *self) {
/* Simple hash of the serialized data */
Py_hash_t h = 0;
for (size_t i = 0; i < self->size; i++) {
h = h * 31 + self->data[i];
}
if (h == -1) h = -2;
return h;
}

PyTypeObject ErlangRefType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "erlang.Ref",
.tp_basicsize = sizeof(ErlangRefObject),
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_dealloc = (destructor)ErlangRef_dealloc,
.tp_repr = (reprfunc)ErlangRef_repr,
.tp_richcompare = ErlangRef_richcompare,
.tp_hash = (hashfunc)ErlangRef_hash,
.tp_doc = "Opaque Erlang reference",
};

/* ============================================================================
* ErlangAtom - Python type for Erlang atoms
*
* Erlang atoms are symbols (like Ruby symbols or Lisp symbols). This type
* allows Python code to explicitly create atoms for message passing.
* ============================================================================ */

static void ErlangAtom_dealloc(ErlangAtomObject *self) {
if (self->name != NULL) {
PyMem_Free(self->name);
}
Py_TYPE(self)->tp_free((PyObject *)self);
}

static PyObject *ErlangAtom_repr(ErlangAtomObject *self) {
return PyUnicode_FromFormat("<erlang.Atom '%s'>", self->name);
}

static PyObject *ErlangAtom_str(ErlangAtomObject *self) {
return PyUnicode_FromString(self->name);
}

static PyObject *ErlangAtom_richcompare(PyObject *a, PyObject *b, int op) {
if (!Py_IS_TYPE(b, &ErlangAtomType)) {
Py_RETURN_NOTIMPLEMENTED;
}
ErlangAtomObject *aa = (ErlangAtomObject *)a;
ErlangAtomObject *ab = (ErlangAtomObject *)b;

int cmp = strcmp(aa->name, ab->name);

switch (op) {
case Py_EQ: return PyBool_FromLong(cmp == 0);
case Py_NE: return PyBool_FromLong(cmp != 0);
case Py_LT: return PyBool_FromLong(cmp < 0);
case Py_LE: return PyBool_FromLong(cmp <= 0);
case Py_GT: return PyBool_FromLong(cmp > 0);
case Py_GE: return PyBool_FromLong(cmp >= 0);
default: Py_RETURN_NOTIMPLEMENTED;
}
}

static Py_hash_t ErlangAtom_hash(ErlangAtomObject *self) {
/* Use Python's string hash for consistency */
PyObject *str = PyUnicode_FromString(self->name);
if (str == NULL) return -1;
Py_hash_t h = PyObject_Hash(str);
Py_DECREF(str);
return h;
}

PyTypeObject ErlangAtomType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "erlang.Atom",
.tp_basicsize = sizeof(ErlangAtomObject),
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_dealloc = (destructor)ErlangAtom_dealloc,
.tp_repr = (reprfunc)ErlangAtom_repr,
.tp_str = (reprfunc)ErlangAtom_str,
.tp_richcompare = ErlangAtom_richcompare,
.tp_hash = (hashfunc)ErlangAtom_hash,
.tp_doc = "Erlang atom (symbol)",
};

/**
* erlang.atom(name) - Create an Erlang atom
*
* Args: name (string)
* Returns: ErlangAtomObject
*/
static PyObject *erlang_atom_impl(PyObject *self, PyObject *args) {
(void)self;
const char *name;
Py_ssize_t name_len;

if (!PyArg_ParseTuple(args, "s#", &name, &name_len)) {
return NULL;
}

/* Validate atom name length (Erlang limit is 255 bytes) */
if (name_len > 255) {
PyErr_SetString(PyExc_ValueError, "Atom name too long (max 255 bytes)");
return NULL;
}

ErlangAtomObject *obj = PyObject_New(ErlangAtomObject, &ErlangAtomType);
if (obj == NULL) {
return NULL;
}

obj->name = PyMem_Malloc(name_len + 1);
if (obj->name == NULL) {
Py_DECREF(obj);
return PyErr_NoMemory();
}

memcpy(obj->name, name, name_len);
obj->name[name_len] = '\0';

return (PyObject *)obj;
}

/* ============================================================================
* ScheduleMarker - marker type for explicit scheduler release
*
Expand Down Expand Up @@ -2002,7 +2164,9 @@ static PyObject *erlang_send_impl(PyObject *self, PyObject *args) {
}

/* Fire-and-forget send */
if (!enif_send(NULL, &pid->pid, msg_env, msg)) {
int send_result = enif_send(NULL, &pid->pid, msg_env, msg);

if (!send_result) {
enif_free_env(msg_env);
PyErr_SetString(get_current_process_error(),
"Failed to send message: process may not exist");
Expand Down Expand Up @@ -2698,6 +2862,11 @@ static PyMethodDef ErlangModuleMethods[] = {
"Call a registered Erlang function.\n\n"
"Usage: erlang.call('func_name', arg1, arg2, ...)\n"
"Returns: The result from the Erlang function."},
{"_atom", erlang_atom_impl, METH_VARARGS,
"Internal: Create an Erlang atom.\n\n"
"Usage: erlang._atom('name')\n"
"Returns: An ErlangAtom object that converts to an Erlang atom.\n"
"NOTE: Use erlang.atom() wrapper instead for safety limits."},
{"send", erlang_send_impl, METH_VARARGS,
"Send a message to an Erlang process (fire-and-forget).\n\n"
"Usage: erlang.send(pid, term)\n"
Expand Down Expand Up @@ -2840,6 +3009,16 @@ static int create_erlang_module(void) {
return -1;
}

/* Initialize ErlangRef type */
if (PyType_Ready(&ErlangRefType) < 0) {
return -1;
}

/* Initialize ErlangAtom type */
if (PyType_Ready(&ErlangAtomType) < 0) {
return -1;
}

/* Initialize ScheduleMarker type */
if (PyType_Ready(&ScheduleMarkerType) < 0) {
return -1;
Expand Down Expand Up @@ -2911,6 +3090,22 @@ static int create_erlang_module(void) {
return -1;
}

/* Add ErlangRef type to module */
Py_INCREF(&ErlangRefType);
if (PyModule_AddObject(module, "Ref", (PyObject *)&ErlangRefType) < 0) {
Py_DECREF(&ErlangRefType);
Py_DECREF(module);
return -1;
}

/* Add ErlangAtom type to module */
Py_INCREF(&ErlangAtomType);
if (PyModule_AddObject(module, "Atom", (PyObject *)&ErlangAtomType) < 0) {
Py_DECREF(&ErlangAtomType);
Py_DECREF(module);
return -1;
}

/* Add ScheduleMarker type to module */
Py_INCREF(&ScheduleMarkerType);
if (PyModule_AddObject(module, "ScheduleMarker", (PyObject *)&ScheduleMarkerType) < 0) {
Expand Down
48 changes: 48 additions & 0 deletions c_src/py_convert.c
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,29 @@ ERL_NIF_TERM py_to_term(ErlNifEnv *env, PyObject *obj) {
return enif_make_pid(env, &pid_obj->pid);
}

/* Handle ErlangRef → Erlang reference (deserialize from binary) */
if (Py_IS_TYPE(obj, &ErlangRefType)) {
ErlangRefObject *ref_obj = (ErlangRefObject *)obj;
ERL_NIF_TERM result;
if (enif_binary_to_term(env, ref_obj->data, ref_obj->size, &result, 0) > 0) {
return result;
}
/* Failed to deserialize - return undefined */
return enif_make_atom(env, "undefined");
}

/* Handle ErlangAtom → Erlang atom */
if (Py_IS_TYPE(obj, &ErlangAtomType)) {
ErlangAtomObject *atom_obj = (ErlangAtomObject *)obj;
ERL_NIF_TERM atom_term;
/* Try existing atom first (no new allocation) */
if (enif_make_existing_atom(env, atom_obj->name, &atom_term, ERL_NIF_LATIN1)) {
return atom_term;
}
/* Atom doesn't exist yet, create it */
return enif_make_atom(env, atom_obj->name);
}

/* Handle NumPy arrays by converting to Python list first */
if (is_numpy_ndarray(obj)) {
PyObject *tolist = PyObject_CallMethod(obj, "tolist", NULL);
Expand Down Expand Up @@ -603,6 +626,31 @@ static PyObject *term_to_py(ErlNifEnv *env, ERL_NIF_TERM term) {
return PyBuffer_from_resource(pybuf, pybuf);
}

/* Check for reference - serialize to binary for round-trip.
* IMPORTANT: This must come AFTER all resource checks, because NIF
* resource terms also satisfy enif_is_ref() but should be handled
* as their specific resource type (PyObj, Channel, Buffer, etc). */
if (enif_is_ref(env, term)) {
ErlNifBinary bin;
if (enif_term_to_binary(env, term, &bin)) {
ErlangRefObject *obj = PyObject_New(ErlangRefObject, &ErlangRefType);
if (obj == NULL) {
enif_release_binary(&bin);
return NULL;
}
obj->data = PyMem_Malloc(bin.size);
if (obj->data == NULL) {
Py_DECREF(obj);
enif_release_binary(&bin);
return NULL;
}
memcpy(obj->data, bin.data, bin.size);
obj->size = bin.size;
enif_release_binary(&bin);
return (PyObject *)obj;
}
}

/* Fallback: return None for unknown types */
Py_RETURN_NONE;
}
Expand Down
Loading
Loading