@@ -2501,6 +2501,76 @@ ERL_NIF_TERM nif_submit_task(ErlNifEnv *env, int argc,
25012501 return ATOM_OK ;
25022502}
25032503
2504+ /**
2505+ * submit_task_with_env(LoopRef, CallerPid, Ref, Module, Func, Args, Kwargs, EnvRef) -> ok | {error, Reason}
2506+ *
2507+ * Like submit_task but includes a process-local env resource reference.
2508+ * The env's globals dict is used for function lookup, allowing functions
2509+ * defined via py:exec() to be called from the event loop.
2510+ */
2511+ ERL_NIF_TERM nif_submit_task_with_env (ErlNifEnv * env , int argc ,
2512+ const ERL_NIF_TERM argv []) {
2513+ (void )argc ;
2514+
2515+ erlang_event_loop_t * loop ;
2516+ if (!enif_get_resource (env , argv [0 ], EVENT_LOOP_RESOURCE_TYPE ,
2517+ (void * * )& loop )) {
2518+ return make_error (env , "invalid_loop" );
2519+ }
2520+
2521+ if (!loop -> task_queue_initialized ) {
2522+ return make_error (env , "task_queue_not_initialized" );
2523+ }
2524+
2525+ /* Validate caller_pid */
2526+ ErlNifPid caller_pid ;
2527+ if (!enif_get_local_pid (env , argv [1 ], & caller_pid )) {
2528+ return make_error (env , "invalid_caller_pid" );
2529+ }
2530+
2531+ /* Create task tuple: {CallerPid, Ref, Module, Func, Args, Kwargs, EnvRef} */
2532+ /* argv[1] = CallerPid, argv[2] = Ref, argv[3] = Module,
2533+ * argv[4] = Func, argv[5] = Args, argv[6] = Kwargs, argv[7] = EnvRef */
2534+ ERL_NIF_TERM task_tuple = enif_make_tuple7 (env ,
2535+ argv [1 ], argv [2 ], argv [3 ], argv [4 ], argv [5 ], argv [6 ], argv [7 ]);
2536+
2537+ /* Serialize to binary */
2538+ ErlNifBinary task_bin ;
2539+ if (!enif_term_to_binary (env , task_tuple , & task_bin )) {
2540+ return make_error (env , "serialization_failed" );
2541+ }
2542+
2543+ /* Thread-safe enqueue */
2544+ pthread_mutex_lock (& loop -> task_queue_mutex );
2545+ int enq_result = enif_ioq_enq_binary (loop -> task_queue , & task_bin , 0 );
2546+ pthread_mutex_unlock (& loop -> task_queue_mutex );
2547+
2548+ if (enq_result != 1 ) {
2549+ enif_release_binary (& task_bin );
2550+ return make_error (env , "enqueue_failed" );
2551+ }
2552+
2553+ /* Increment task count */
2554+ atomic_fetch_add (& loop -> task_count , 1 );
2555+
2556+ /* Coalesced wakeup (uvloop-style) */
2557+ if (loop -> has_worker ) {
2558+ if (!atomic_exchange (& loop -> task_wake_pending , true)) {
2559+ ErlNifEnv * msg_env = enif_alloc_env ();
2560+ if (msg_env != NULL ) {
2561+ if (ATOM_TASK_READY == 0 ) {
2562+ ATOM_TASK_READY = enif_make_atom (msg_env , "task_ready" );
2563+ }
2564+ ERL_NIF_TERM msg = enif_make_atom (msg_env , "task_ready" );
2565+ enif_send (NULL , & loop -> worker_pid , msg_env , msg );
2566+ enif_free_env (msg_env );
2567+ }
2568+ }
2569+ }
2570+
2571+ return ATOM_OK ;
2572+ }
2573+
25042574/**
25052575 * Maximum tasks to dequeue in one batch before acquiring GIL.
25062576 * This bounds memory usage while still amortizing GIL acquisition cost.
@@ -2789,10 +2859,12 @@ ERL_NIF_TERM nif_process_ready_tasks(ErlNifEnv *env, int argc,
27892859 ErlNifEnv * term_env = tasks [task_idx ].term_env ;
27902860 ERL_NIF_TERM task_term = tasks [task_idx ].task_term ;
27912861
2792- /* Extract: {CallerPid, Ref, Module, Func, Args, Kwargs} */
2862+ /* Extract: {CallerPid, Ref, Module, Func, Args, Kwargs} or
2863+ * {CallerPid, Ref, Module, Func, Args, Kwargs, EnvRef} */
27932864 int arity ;
27942865 const ERL_NIF_TERM * tuple_elems ;
2795- if (!enif_get_tuple (term_env , task_term , & arity , & tuple_elems ) || arity != 6 ) {
2866+ if (!enif_get_tuple (term_env , task_term , & arity , & tuple_elems ) ||
2867+ (arity != 6 && arity != 7 )) {
27962868 enif_free_env (term_env );
27972869 continue ;
27982870 }
@@ -2810,6 +2882,16 @@ ERL_NIF_TERM nif_process_ready_tasks(ErlNifEnv *env, int argc,
28102882 continue ;
28112883 }
28122884
2885+ /* Check for env resource (7-tuple) */
2886+ py_env_resource_t * task_env = NULL ;
2887+ if (arity == 7 ) {
2888+ /* Try to get env resource from tuple_elems[6] */
2889+ if (!enif_get_resource (term_env , tuple_elems [6 ],
2890+ get_env_resource_type (), (void * * )& task_env )) {
2891+ task_env = NULL ; /* Invalid env ref, continue without it */
2892+ }
2893+ }
2894+
28132895 /* Convert module/func to C strings */
28142896 char * module_name = enif_alloc (module_bin .size + 1 );
28152897 char * func_name = enif_alloc (func_bin .size + 1 );
@@ -2824,11 +2906,27 @@ ERL_NIF_TERM nif_process_ready_tasks(ErlNifEnv *env, int argc,
28242906 memcpy (func_name , func_bin .data , func_bin .size );
28252907 func_name [func_bin .size ] = '\0' ;
28262908
2827- /* Look up namespace for caller process (only exists if they called exec/eval ) */
2909+ /* Look up namespace for caller process (used for reentrant calls ) */
28282910 process_namespace_t * ns = lookup_process_namespace (loop , & caller_pid );
28292911
2830- /* Look up function (checks process namespace for __main__, then cache/import) */
2831- PyObject * func = get_function_for_task (loop , ns , module_name , func_name );
2912+ /* Look up function - check task_env first, then process namespace, then import */
2913+ PyObject * func = NULL ;
2914+
2915+ /* First, check the passed env's globals (from py:exec) */
2916+ if (task_env != NULL && task_env -> globals != NULL ) {
2917+ if (strcmp (module_name , "__main__" ) == 0 ||
2918+ strcmp (module_name , "_process_" ) == 0 ) {
2919+ func = PyDict_GetItemString (task_env -> globals , func_name );
2920+ if (func != NULL ) {
2921+ Py_INCREF (func );
2922+ }
2923+ }
2924+ }
2925+
2926+ /* Fallback to process namespace and cache/import */
2927+ if (func == NULL ) {
2928+ func = get_function_for_task (loop , ns , module_name , func_name );
2929+ }
28322930
28332931 enif_free (module_name );
28342932 enif_free (func_name );
0 commit comments