@@ -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