Skip to content
Open
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
246 changes: 246 additions & 0 deletions c_src/py_nif.c
Original file line number Diff line number Diff line change
Expand Up @@ -4707,6 +4707,249 @@
return enif_make_tuple2(env, ATOM_OK, ref);
}

/**
* @brief Create a new Python environment with initialization code
*
* nif_new_env_with_code(Code) -> {ok, EnvRef} | {error, Reason}
*
* Creates a new Python environment with globals/locals dicts and executes
* the provided initialization code. The environment can be used independently
* of any context - it runs in the main interpreter.
*
* This is useful for creating named environments that can be shared
* across processes and set as the current environment for py:eval/exec.
*/
static ERL_NIF_TERM nif_new_env_with_code(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;

if (!runtime_is_running()) {
return make_error(env, "python_not_running");
}

ErlNifBinary code_bin;
if (!enif_inspect_binary(env, argv[0], &code_bin)) {
return make_error(env, "invalid_code");
}

/* Allocate environment resource */
py_env_resource_t *res = enif_alloc_resource(PY_ENV_RESOURCE_TYPE,
sizeof(py_env_resource_t));
if (res == NULL) {
return make_error(env, "alloc_failed");
}

res->globals = NULL;
res->locals = NULL;
res->interp_id = 0;
res->pool_slot = -1;

/* Acquire GIL for main interpreter */
PyGILState_STATE gstate = PyGILState_Ensure();

/* Store interpreter info for destructor */
PyInterpreterState *interp = PyInterpreterState_Get();
if (interp != NULL) {
res->interp_id = PyInterpreterState_GetID(interp);
}

/* Create globals dict with builtins */
res->globals = PyDict_New();
if (res->globals == NULL) {
PyGILState_Release(gstate);
enif_release_resource(res);
return make_error(env, "globals_failed");
}

/* Add __builtins__ */
PyObject *builtins = PyEval_GetBuiltins();
if (builtins != NULL) {
PyDict_SetItemString(res->globals, "__builtins__", builtins);
}

/* Add __name__ = '__main__' */
PyObject *main_name = PyUnicode_FromString("__main__");
if (main_name != NULL) {
PyDict_SetItemString(res->globals, "__name__", main_name);
Py_DECREF(main_name);
}

/* Add erlang module */
PyObject *erlang = PyImport_ImportModule("erlang");
if (erlang != NULL) {
PyDict_SetItemString(res->globals, "erlang", erlang);
Py_DECREF(erlang);
}

/* Use the same dict for locals (module-level execution) */
res->locals = res->globals;
Py_INCREF(res->locals);

/* Execute initialization code */
char *code = enif_alloc(code_bin.size + 1);
if (code == NULL) {
Py_DECREF(res->globals);
Py_DECREF(res->locals);
res->globals = NULL;
res->locals = NULL;
PyGILState_Release(gstate);
enif_release_resource(res);
return make_error(env, "alloc_failed");
}
memcpy(code, code_bin.data, code_bin.size);
code[code_bin.size] = '\0';

PyObject *py_result = PyRun_String(code, Py_file_input, res->globals, res->globals);
enif_free(code);

if (py_result == NULL) {
ERL_NIF_TERM error = make_py_error(env);
Py_DECREF(res->globals);
Py_DECREF(res->locals);
res->globals = NULL;
res->locals = NULL;
PyGILState_Release(gstate);
enif_release_resource(res);
return error;
}
Py_DECREF(py_result);

PyGILState_Release(gstate);

ERL_NIF_TERM ref = enif_make_resource(env, res);
enif_release_resource(res); /* Ref now owns it */

return enif_make_tuple2(env, ATOM_OK, ref);
}

/**
* @brief Evaluate a Python expression using an environment
*
* nif_env_eval(EnvRef, Code) -> {ok, Result} | {error, Reason}
*
* Evaluates a Python expression using the provided environment's globals.
* This allows evaluation against a named environment without needing a context.
*/
static ERL_NIF_TERM nif_env_eval(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;

if (!runtime_is_running()) {
return make_error(env, "python_not_running");
}

py_env_resource_t *penv;
if (!enif_get_resource(env, argv[0], PY_ENV_RESOURCE_TYPE, (void **)&penv)) {
return make_error(env, "invalid_env");
}

ErlNifBinary code_bin;
if (!enif_inspect_binary(env, argv[1], &code_bin)) {
return make_error(env, "invalid_code");
}

if (penv->globals == NULL) {
return make_error(env, "env_not_initialized");
}

/* Acquire GIL */
PyGILState_STATE gstate = PyGILState_Ensure();

/* Verify interpreter ownership */
PyInterpreterState *current_interp = PyInterpreterState_Get();
if (current_interp != NULL && penv->interp_id != PyInterpreterState_GetID(current_interp)) {
PyGILState_Release(gstate);
return make_error(env, "wrong_interpreter");
}

/* Copy code to null-terminated string */
char *code = enif_alloc(code_bin.size + 1);
if (code == NULL) {
PyGILState_Release(gstate);
return make_error(env, "alloc_failed");
}
memcpy(code, code_bin.data, code_bin.size);
code[code_bin.size] = '\0';

/* Evaluate expression */
PyObject *py_result = PyRun_String(code, Py_eval_input, penv->globals, penv->globals);
enif_free(code);

ERL_NIF_TERM result;
if (py_result == NULL) {
result = make_py_error(env);
} else {
ERL_NIF_TERM term_result = py_to_term(env, py_result);
Py_DECREF(py_result);
result = enif_make_tuple2(env, ATOM_OK, term_result);
}

PyGILState_Release(gstate);
return result;
}

/**
* @brief Execute Python statements using an environment
*
* nif_env_exec(EnvRef, Code) -> ok | {error, Reason}
*
* Executes Python statements using the provided environment's globals.
* This allows execution against a named environment without needing a context.
*/
static ERL_NIF_TERM nif_env_exec(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;

if (!runtime_is_running()) {
return make_error(env, "python_not_running");
}

py_env_resource_t *penv;
if (!enif_get_resource(env, argv[0], PY_ENV_RESOURCE_TYPE, (void **)&penv)) {
return make_error(env, "invalid_env");
}

ErlNifBinary code_bin;
if (!enif_inspect_binary(env, argv[1], &code_bin)) {
return make_error(env, "invalid_code");
}

if (penv->globals == NULL) {
return make_error(env, "env_not_initialized");
}

/* Acquire GIL */
PyGILState_STATE gstate = PyGILState_Ensure();

/* Verify interpreter ownership */
PyInterpreterState *current_interp = PyInterpreterState_Get();
if (current_interp != NULL && penv->interp_id != PyInterpreterState_GetID(current_interp)) {
PyGILState_Release(gstate);
return make_error(env, "wrong_interpreter");
}

/* Copy code to null-terminated string */
char *code = enif_alloc(code_bin.size + 1);
if (code == NULL) {
PyGILState_Release(gstate);
return make_error(env, "alloc_failed");
}
memcpy(code, code_bin.data, code_bin.size);
code[code_bin.size] = '\0';

/* Execute statements */
PyObject *py_result = PyRun_String(code, Py_file_input, penv->globals, penv->globals);
enif_free(code);

ERL_NIF_TERM result;
if (py_result == NULL) {
result = make_py_error(env);
} else {
Py_DECREF(py_result);
result = ATOM_OK;
}

PyGILState_Release(gstate);
return result;
}

/**
* @brief Execute Python statements using a process-local environment
*
Expand Down Expand Up @@ -6359,7 +6602,7 @@
if (write(w->cmd_pipe[1], &header, sizeof(header)) == sizeof(header)) {
/* Wait for response */
owngil_header_t resp;
read(w->result_pipe[0], &resp, sizeof(resp));

Check warning on line 6605 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / Documentation

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6605 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / Lint

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6605 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / Free-threaded Python 3.13t

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6605 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / OTP 27.0 / Python 3.12 / ubuntu-24.04

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6605 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / OTP 27.0 / Python 3.13 / ubuntu-24.04

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6605 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / ASan / Python 3.13

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]

Check warning on line 6605 in c_src/py_nif.c

View workflow job for this annotation

GitHub Actions / ASan / Python 3.12

ignoring return value of ‘read’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
}

pthread_mutex_unlock(&w->dispatch_mutex);
Expand Down Expand Up @@ -6836,6 +7079,9 @@
{"context_eval", 4, nif_context_eval_with_env, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"context_call", 6, nif_context_call_with_env, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"create_local_env", 1, nif_create_local_env, 0},
{"new_env_with_code", 1, nif_new_env_with_code, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"env_eval", 2, nif_env_eval, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"env_exec", 2, nif_env_exec, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"context_call_method", 4, nif_context_call_method, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"context_to_term", 1, nif_context_to_term, 0},
{"context_interp_id", 1, nif_context_interp_id, 0},
Expand Down
Loading
Loading