Skip to content

Commit 4c9ff1a

Browse files
committed
Add Named Environment API, replace preload with named envs
Add py:new_env/1,2 to create reusable Python environments with initialization code. Environments can be named for global lookup via persistent_term. New API: - py:new_env(Code) / py:new_env(Code, #{name => atom()}) - py:set_env(Name | Ref) - set current env for process - py:get_env() / py:get_env(Name) - get current/named env - py:list_envs() - list all named environments - py:destroy_env(Ref) - remove from registry When a named env is set, py:eval/exec and py_event_loop_pool:eval/exec use it directly via NIF instead of routing through contexts. Remove preload API from py_event_loop_pool in favor of named environments which provide more flexibility.
1 parent 93a32ad commit 4c9ff1a

File tree

6 files changed

+601
-37
lines changed

6 files changed

+601
-37
lines changed

c_src/py_nif.c

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4707,6 +4707,249 @@ static ERL_NIF_TERM nif_create_local_env(ErlNifEnv *env, int argc, const ERL_NIF
47074707
return enif_make_tuple2(env, ATOM_OK, ref);
47084708
}
47094709

4710+
/**
4711+
* @brief Create a new Python environment with initialization code
4712+
*
4713+
* nif_new_env_with_code(Code) -> {ok, EnvRef} | {error, Reason}
4714+
*
4715+
* Creates a new Python environment with globals/locals dicts and executes
4716+
* the provided initialization code. The environment can be used independently
4717+
* of any context - it runs in the main interpreter.
4718+
*
4719+
* This is useful for creating named environments that can be shared
4720+
* across processes and set as the current environment for py:eval/exec.
4721+
*/
4722+
static ERL_NIF_TERM nif_new_env_with_code(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
4723+
(void)argc;
4724+
4725+
if (!runtime_is_running()) {
4726+
return make_error(env, "python_not_running");
4727+
}
4728+
4729+
ErlNifBinary code_bin;
4730+
if (!enif_inspect_binary(env, argv[0], &code_bin)) {
4731+
return make_error(env, "invalid_code");
4732+
}
4733+
4734+
/* Allocate environment resource */
4735+
py_env_resource_t *res = enif_alloc_resource(PY_ENV_RESOURCE_TYPE,
4736+
sizeof(py_env_resource_t));
4737+
if (res == NULL) {
4738+
return make_error(env, "alloc_failed");
4739+
}
4740+
4741+
res->globals = NULL;
4742+
res->locals = NULL;
4743+
res->interp_id = 0;
4744+
res->pool_slot = -1;
4745+
4746+
/* Acquire GIL for main interpreter */
4747+
PyGILState_STATE gstate = PyGILState_Ensure();
4748+
4749+
/* Store interpreter info for destructor */
4750+
PyInterpreterState *interp = PyInterpreterState_Get();
4751+
if (interp != NULL) {
4752+
res->interp_id = PyInterpreterState_GetID(interp);
4753+
}
4754+
4755+
/* Create globals dict with builtins */
4756+
res->globals = PyDict_New();
4757+
if (res->globals == NULL) {
4758+
PyGILState_Release(gstate);
4759+
enif_release_resource(res);
4760+
return make_error(env, "globals_failed");
4761+
}
4762+
4763+
/* Add __builtins__ */
4764+
PyObject *builtins = PyEval_GetBuiltins();
4765+
if (builtins != NULL) {
4766+
PyDict_SetItemString(res->globals, "__builtins__", builtins);
4767+
}
4768+
4769+
/* Add __name__ = '__main__' */
4770+
PyObject *main_name = PyUnicode_FromString("__main__");
4771+
if (main_name != NULL) {
4772+
PyDict_SetItemString(res->globals, "__name__", main_name);
4773+
Py_DECREF(main_name);
4774+
}
4775+
4776+
/* Add erlang module */
4777+
PyObject *erlang = PyImport_ImportModule("erlang");
4778+
if (erlang != NULL) {
4779+
PyDict_SetItemString(res->globals, "erlang", erlang);
4780+
Py_DECREF(erlang);
4781+
}
4782+
4783+
/* Use the same dict for locals (module-level execution) */
4784+
res->locals = res->globals;
4785+
Py_INCREF(res->locals);
4786+
4787+
/* Execute initialization code */
4788+
char *code = enif_alloc(code_bin.size + 1);
4789+
if (code == NULL) {
4790+
Py_DECREF(res->globals);
4791+
Py_DECREF(res->locals);
4792+
res->globals = NULL;
4793+
res->locals = NULL;
4794+
PyGILState_Release(gstate);
4795+
enif_release_resource(res);
4796+
return make_error(env, "alloc_failed");
4797+
}
4798+
memcpy(code, code_bin.data, code_bin.size);
4799+
code[code_bin.size] = '\0';
4800+
4801+
PyObject *py_result = PyRun_String(code, Py_file_input, res->globals, res->globals);
4802+
enif_free(code);
4803+
4804+
if (py_result == NULL) {
4805+
ERL_NIF_TERM error = make_py_error(env);
4806+
Py_DECREF(res->globals);
4807+
Py_DECREF(res->locals);
4808+
res->globals = NULL;
4809+
res->locals = NULL;
4810+
PyGILState_Release(gstate);
4811+
enif_release_resource(res);
4812+
return error;
4813+
}
4814+
Py_DECREF(py_result);
4815+
4816+
PyGILState_Release(gstate);
4817+
4818+
ERL_NIF_TERM ref = enif_make_resource(env, res);
4819+
enif_release_resource(res); /* Ref now owns it */
4820+
4821+
return enif_make_tuple2(env, ATOM_OK, ref);
4822+
}
4823+
4824+
/**
4825+
* @brief Evaluate a Python expression using an environment
4826+
*
4827+
* nif_env_eval(EnvRef, Code) -> {ok, Result} | {error, Reason}
4828+
*
4829+
* Evaluates a Python expression using the provided environment's globals.
4830+
* This allows evaluation against a named environment without needing a context.
4831+
*/
4832+
static ERL_NIF_TERM nif_env_eval(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
4833+
(void)argc;
4834+
4835+
if (!runtime_is_running()) {
4836+
return make_error(env, "python_not_running");
4837+
}
4838+
4839+
py_env_resource_t *penv;
4840+
if (!enif_get_resource(env, argv[0], PY_ENV_RESOURCE_TYPE, (void **)&penv)) {
4841+
return make_error(env, "invalid_env");
4842+
}
4843+
4844+
ErlNifBinary code_bin;
4845+
if (!enif_inspect_binary(env, argv[1], &code_bin)) {
4846+
return make_error(env, "invalid_code");
4847+
}
4848+
4849+
if (penv->globals == NULL) {
4850+
return make_error(env, "env_not_initialized");
4851+
}
4852+
4853+
/* Acquire GIL */
4854+
PyGILState_STATE gstate = PyGILState_Ensure();
4855+
4856+
/* Verify interpreter ownership */
4857+
PyInterpreterState *current_interp = PyInterpreterState_Get();
4858+
if (current_interp != NULL && penv->interp_id != PyInterpreterState_GetID(current_interp)) {
4859+
PyGILState_Release(gstate);
4860+
return make_error(env, "wrong_interpreter");
4861+
}
4862+
4863+
/* Copy code to null-terminated string */
4864+
char *code = enif_alloc(code_bin.size + 1);
4865+
if (code == NULL) {
4866+
PyGILState_Release(gstate);
4867+
return make_error(env, "alloc_failed");
4868+
}
4869+
memcpy(code, code_bin.data, code_bin.size);
4870+
code[code_bin.size] = '\0';
4871+
4872+
/* Evaluate expression */
4873+
PyObject *py_result = PyRun_String(code, Py_eval_input, penv->globals, penv->globals);
4874+
enif_free(code);
4875+
4876+
ERL_NIF_TERM result;
4877+
if (py_result == NULL) {
4878+
result = make_py_error(env);
4879+
} else {
4880+
ERL_NIF_TERM term_result = py_to_term(env, py_result);
4881+
Py_DECREF(py_result);
4882+
result = enif_make_tuple2(env, ATOM_OK, term_result);
4883+
}
4884+
4885+
PyGILState_Release(gstate);
4886+
return result;
4887+
}
4888+
4889+
/**
4890+
* @brief Execute Python statements using an environment
4891+
*
4892+
* nif_env_exec(EnvRef, Code) -> ok | {error, Reason}
4893+
*
4894+
* Executes Python statements using the provided environment's globals.
4895+
* This allows execution against a named environment without needing a context.
4896+
*/
4897+
static ERL_NIF_TERM nif_env_exec(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
4898+
(void)argc;
4899+
4900+
if (!runtime_is_running()) {
4901+
return make_error(env, "python_not_running");
4902+
}
4903+
4904+
py_env_resource_t *penv;
4905+
if (!enif_get_resource(env, argv[0], PY_ENV_RESOURCE_TYPE, (void **)&penv)) {
4906+
return make_error(env, "invalid_env");
4907+
}
4908+
4909+
ErlNifBinary code_bin;
4910+
if (!enif_inspect_binary(env, argv[1], &code_bin)) {
4911+
return make_error(env, "invalid_code");
4912+
}
4913+
4914+
if (penv->globals == NULL) {
4915+
return make_error(env, "env_not_initialized");
4916+
}
4917+
4918+
/* Acquire GIL */
4919+
PyGILState_STATE gstate = PyGILState_Ensure();
4920+
4921+
/* Verify interpreter ownership */
4922+
PyInterpreterState *current_interp = PyInterpreterState_Get();
4923+
if (current_interp != NULL && penv->interp_id != PyInterpreterState_GetID(current_interp)) {
4924+
PyGILState_Release(gstate);
4925+
return make_error(env, "wrong_interpreter");
4926+
}
4927+
4928+
/* Copy code to null-terminated string */
4929+
char *code = enif_alloc(code_bin.size + 1);
4930+
if (code == NULL) {
4931+
PyGILState_Release(gstate);
4932+
return make_error(env, "alloc_failed");
4933+
}
4934+
memcpy(code, code_bin.data, code_bin.size);
4935+
code[code_bin.size] = '\0';
4936+
4937+
/* Execute statements */
4938+
PyObject *py_result = PyRun_String(code, Py_file_input, penv->globals, penv->globals);
4939+
enif_free(code);
4940+
4941+
ERL_NIF_TERM result;
4942+
if (py_result == NULL) {
4943+
result = make_py_error(env);
4944+
} else {
4945+
Py_DECREF(py_result);
4946+
result = ATOM_OK;
4947+
}
4948+
4949+
PyGILState_Release(gstate);
4950+
return result;
4951+
}
4952+
47104953
/**
47114954
* @brief Execute Python statements using a process-local environment
47124955
*
@@ -6836,6 +7079,9 @@ static ErlNifFunc nif_funcs[] = {
68367079
{"context_eval", 4, nif_context_eval_with_env, ERL_NIF_DIRTY_JOB_CPU_BOUND},
68377080
{"context_call", 6, nif_context_call_with_env, ERL_NIF_DIRTY_JOB_CPU_BOUND},
68387081
{"create_local_env", 1, nif_create_local_env, 0},
7082+
{"new_env_with_code", 1, nif_new_env_with_code, ERL_NIF_DIRTY_JOB_CPU_BOUND},
7083+
{"env_eval", 2, nif_env_eval, ERL_NIF_DIRTY_JOB_CPU_BOUND},
7084+
{"env_exec", 2, nif_env_exec, ERL_NIF_DIRTY_JOB_CPU_BOUND},
68397085
{"context_call_method", 4, nif_context_call_method, ERL_NIF_DIRTY_JOB_CPU_BOUND},
68407086
{"context_to_term", 1, nif_context_to_term, 0},
68417087
{"context_interp_id", 1, nif_context_interp_id, 0},

0 commit comments

Comments
 (0)