@@ -3168,6 +3168,96 @@ static void owngil_execute_create_local_env(py_context_t *ctx) {
31683168 ctx -> response_ok = true;
31693169}
31703170
3171+ /**
3172+ * @brief Execute apply_imports in OWN_GIL context
3173+ *
3174+ * Applies a list of imports to the interpreter's sys.modules.
3175+ * The imports list is passed via request_term.
3176+ *
3177+ * Note: OWN_GIL contexts have their own dedicated interpreter,
3178+ * so sys.modules is per-context in this mode.
3179+ */
3180+ static void owngil_execute_apply_imports (py_context_t * ctx ) {
3181+ /* Process each import from request_term */
3182+ ERL_NIF_TERM head , tail = ctx -> request_term ;
3183+ int arity ;
3184+ const ERL_NIF_TERM * tuple ;
3185+
3186+ while (enif_get_list_cell (ctx -> shared_env , tail , & head , & tail )) {
3187+ if (!enif_get_tuple (ctx -> shared_env , head , & arity , & tuple ) || arity != 2 ) {
3188+ continue ;
3189+ }
3190+
3191+ ErlNifBinary module_bin ;
3192+ if (!enif_inspect_binary (ctx -> shared_env , tuple [0 ], & module_bin )) {
3193+ continue ;
3194+ }
3195+
3196+ /* Convert to C string */
3197+ char * module_name = enif_alloc (module_bin .size + 1 );
3198+ if (module_name == NULL ) continue ;
3199+ memcpy (module_name , module_bin .data , module_bin .size );
3200+ module_name [module_bin .size ] = '\0' ;
3201+
3202+ /* Skip __main__ */
3203+ if (strcmp (module_name , "__main__" ) == 0 ) {
3204+ enif_free (module_name );
3205+ continue ;
3206+ }
3207+
3208+ /* Import the module - caches in this interpreter's sys.modules */
3209+ PyObject * mod = PyImport_ImportModule (module_name );
3210+ if (mod != NULL ) {
3211+ Py_DECREF (mod ); /* sys.modules holds the reference */
3212+ } else {
3213+ /* Clear error - import failure is not fatal */
3214+ PyErr_Clear ();
3215+ }
3216+
3217+ enif_free (module_name );
3218+ }
3219+
3220+ ctx -> response_term = enif_make_atom (ctx -> shared_env , "ok" );
3221+ ctx -> response_ok = true;
3222+ }
3223+
3224+ /**
3225+ * @brief Execute flush_imports in OWN_GIL context
3226+ *
3227+ * Clears the per-context module cache optimization layer.
3228+ * Does NOT clear sys.modules (that would break Python).
3229+ */
3230+ static void owngil_execute_flush_imports (py_context_t * ctx ) {
3231+ /* Remove specified modules from sys.modules */
3232+ PyObject * sys_modules = PyImport_GetModuleDict (); /* Borrowed ref */
3233+ if (sys_modules != NULL ) {
3234+ ERL_NIF_TERM head , tail = ctx -> request_data ;
3235+ while (enif_get_list_cell (ctx -> shared_env , tail , & head , & tail )) {
3236+ ErlNifBinary mod_bin ;
3237+ if (enif_inspect_binary (ctx -> shared_env , head , & mod_bin )) {
3238+ char * mod_name = enif_alloc (mod_bin .size + 1 );
3239+ if (mod_name != NULL ) {
3240+ memcpy (mod_name , mod_bin .data , mod_bin .size );
3241+ mod_name [mod_bin .size ] = '\0' ;
3242+ /* Remove module from sys.modules if present */
3243+ if (PyDict_GetItemString (sys_modules , mod_name ) != NULL ) {
3244+ PyDict_DelItemString (sys_modules , mod_name );
3245+ }
3246+ enif_free (mod_name );
3247+ }
3248+ }
3249+ }
3250+ }
3251+
3252+ /* Clear the per-context optimization cache if it exists */
3253+ if (ctx -> module_cache != NULL ) {
3254+ PyDict_Clear (ctx -> module_cache );
3255+ }
3256+
3257+ ctx -> response_term = enif_make_atom (ctx -> shared_env , "ok" );
3258+ ctx -> response_ok = true;
3259+ }
3260+
31713261/**
31723262 * @brief Execute a request based on its type
31733263 */
@@ -3203,6 +3293,12 @@ static void owngil_execute_request(py_context_t *ctx) {
32033293 case CTX_REQ_CREATE_LOCAL_ENV :
32043294 owngil_execute_create_local_env (ctx );
32053295 break ;
3296+ case CTX_REQ_APPLY_IMPORTS :
3297+ owngil_execute_apply_imports (ctx );
3298+ break ;
3299+ case CTX_REQ_FLUSH_IMPORTS :
3300+ owngil_execute_flush_imports (ctx );
3301+ break ;
32063302 default :
32073303 ctx -> response_term = enif_make_tuple2 (ctx -> shared_env ,
32083304 enif_make_atom (ctx -> shared_env , "error" ),
@@ -3768,6 +3864,94 @@ static ERL_NIF_TERM dispatch_create_local_env_to_owngil(
37683864 return result ;
37693865}
37703866
3867+ /**
3868+ * @brief Dispatch apply_imports to OWN_GIL worker thread
3869+ *
3870+ * @param env NIF environment
3871+ * @param ctx Context resource
3872+ * @param imports_term List of {ModuleBin, FuncBin | all} tuples
3873+ * @return ok | {error, Reason}
3874+ */
3875+ static ERL_NIF_TERM dispatch_apply_imports_to_owngil (
3876+ ErlNifEnv * env , py_context_t * ctx , ERL_NIF_TERM imports_term
3877+ ) {
3878+ if (!atomic_load (& ctx -> thread_running )) {
3879+ return make_error (env , "thread_not_running" );
3880+ }
3881+
3882+ pthread_mutex_lock (& ctx -> request_mutex );
3883+
3884+ enif_clear_env (ctx -> shared_env );
3885+ ctx -> request_term = enif_make_copy (ctx -> shared_env , imports_term );
3886+ ctx -> request_type = CTX_REQ_APPLY_IMPORTS ;
3887+
3888+ pthread_cond_signal (& ctx -> request_ready );
3889+
3890+ /* Wait for response with timeout */
3891+ struct timespec deadline ;
3892+ clock_gettime (CLOCK_REALTIME , & deadline );
3893+ deadline .tv_sec += OWNGIL_DISPATCH_TIMEOUT_SECS ;
3894+
3895+ while (ctx -> request_type != CTX_REQ_NONE ) {
3896+ int rc = pthread_cond_timedwait (& ctx -> response_ready , & ctx -> request_mutex , & deadline );
3897+ if (rc == ETIMEDOUT ) {
3898+ atomic_store (& ctx -> thread_running , false);
3899+ pthread_mutex_unlock (& ctx -> request_mutex );
3900+ fprintf (stderr , "OWN_GIL apply_imports dispatch timeout: worker thread unresponsive\n" );
3901+ return make_error (env , "worker_timeout" );
3902+ }
3903+ }
3904+
3905+ ERL_NIF_TERM result = enif_make_copy (env , ctx -> response_term );
3906+ pthread_mutex_unlock (& ctx -> request_mutex );
3907+
3908+ return result ;
3909+ }
3910+
3911+ /**
3912+ * @brief Dispatch flush_imports to OWN_GIL worker thread
3913+ *
3914+ * @param env NIF environment
3915+ * @param ctx Context resource
3916+ * @param modules_list List of module names to remove from sys.modules
3917+ * @return ok
3918+ */
3919+ static ERL_NIF_TERM dispatch_flush_imports_to_owngil (
3920+ ErlNifEnv * env , py_context_t * ctx , ERL_NIF_TERM modules_list
3921+ ) {
3922+ if (!atomic_load (& ctx -> thread_running )) {
3923+ return make_error (env , "thread_not_running" );
3924+ }
3925+
3926+ pthread_mutex_lock (& ctx -> request_mutex );
3927+
3928+ enif_clear_env (ctx -> shared_env );
3929+ ctx -> request_type = CTX_REQ_FLUSH_IMPORTS ;
3930+ ctx -> request_data = enif_make_copy (ctx -> shared_env , modules_list );
3931+
3932+ pthread_cond_signal (& ctx -> request_ready );
3933+
3934+ /* Wait for response with timeout */
3935+ struct timespec deadline ;
3936+ clock_gettime (CLOCK_REALTIME , & deadline );
3937+ deadline .tv_sec += OWNGIL_DISPATCH_TIMEOUT_SECS ;
3938+
3939+ while (ctx -> request_type != CTX_REQ_NONE ) {
3940+ int rc = pthread_cond_timedwait (& ctx -> response_ready , & ctx -> request_mutex , & deadline );
3941+ if (rc == ETIMEDOUT ) {
3942+ atomic_store (& ctx -> thread_running , false);
3943+ pthread_mutex_unlock (& ctx -> request_mutex );
3944+ fprintf (stderr , "OWN_GIL flush_imports dispatch timeout: worker thread unresponsive\n" );
3945+ return make_error (env , "worker_timeout" );
3946+ }
3947+ }
3948+
3949+ ERL_NIF_TERM result = enif_make_copy (env , ctx -> response_term );
3950+ pthread_mutex_unlock (& ctx -> request_mutex );
3951+
3952+ return result ;
3953+ }
3954+
37713955#endif /* HAVE_SUBINTERPRETERS */
37723956
37733957/**
@@ -3786,6 +3970,9 @@ static int owngil_context_init(py_context_t *ctx) {
37863970 atomic_store (& ctx -> init_error , false);
37873971 atomic_store (& ctx -> shutdown_requested , false);
37883972 ctx -> request_type = CTX_REQ_NONE ;
3973+ ctx -> request_term = 0 ;
3974+ ctx -> request_data = 0 ;
3975+ ctx -> response_term = 0 ;
37893976 ctx -> response_ok = false;
37903977
37913978 /* Initialize mutex and condition variables */
@@ -4735,6 +4922,158 @@ static ERL_NIF_TERM nif_create_local_env(ErlNifEnv *env, int argc, const ERL_NIF
47354922 return enif_make_tuple2 (env , ATOM_OK , ref );
47364923}
47374924
4925+ /**
4926+ * @brief Apply a list of imports to an interpreter's sys.modules
4927+ *
4928+ * nif_interp_apply_imports(Ref, Imports) -> ok | {error, Reason}
4929+ *
4930+ * Imports: [{ModuleBin, FuncBin | 'all'}, ...]
4931+ * Imports modules into the interpreter's sys.modules (shared by all
4932+ * contexts/loops using this interpreter).
4933+ *
4934+ * Note: This imports into the INTERPRETER's module cache (sys.modules),
4935+ * not a per-context cache. All contexts using this interpreter will
4936+ * see the imported modules.
4937+ */
4938+ static ERL_NIF_TERM nif_interp_apply_imports (ErlNifEnv * env , int argc , const ERL_NIF_TERM argv []) {
4939+ (void )argc ;
4940+ py_context_t * ctx ;
4941+
4942+ if (!runtime_is_running ()) {
4943+ return make_error (env , "python_not_running" );
4944+ }
4945+
4946+ if (!enif_get_resource (env , argv [0 ], PY_CONTEXT_RESOURCE_TYPE , (void * * )& ctx )) {
4947+ return make_error (env , "invalid_context" );
4948+ }
4949+
4950+ if (ctx -> destroyed ) {
4951+ return make_error (env , "context_destroyed" );
4952+ }
4953+
4954+ #ifdef HAVE_SUBINTERPRETERS
4955+ /* OWN_GIL mode: dispatch to the dedicated thread */
4956+ if (ctx -> uses_own_gil ) {
4957+ return dispatch_apply_imports_to_owngil (env , ctx , argv [1 ]);
4958+ }
4959+ #endif
4960+
4961+ py_context_guard_t guard = py_context_acquire (ctx );
4962+ if (!guard .acquired ) {
4963+ return make_error (env , "acquire_failed" );
4964+ }
4965+
4966+ /* Process each import - imports go into interpreter's sys.modules */
4967+ ERL_NIF_TERM head , tail = argv [1 ];
4968+ int arity ;
4969+ const ERL_NIF_TERM * tuple ;
4970+
4971+ while (enif_get_list_cell (env , tail , & head , & tail )) {
4972+ if (!enif_get_tuple (env , head , & arity , & tuple ) || arity != 2 ) {
4973+ continue ;
4974+ }
4975+
4976+ ErlNifBinary module_bin ;
4977+ if (!enif_inspect_binary (env , tuple [0 ], & module_bin )) {
4978+ continue ;
4979+ }
4980+
4981+ /* Convert to C string */
4982+ char * module_name = enif_alloc (module_bin .size + 1 );
4983+ if (module_name == NULL ) continue ;
4984+ memcpy (module_name , module_bin .data , module_bin .size );
4985+ module_name [module_bin .size ] = '\0' ;
4986+
4987+ /* Skip __main__ */
4988+ if (strcmp (module_name , "__main__" ) == 0 ) {
4989+ enif_free (module_name );
4990+ continue ;
4991+ }
4992+
4993+ /* Import the module - this caches in interpreter's sys.modules
4994+ * which is shared by all contexts using this interpreter */
4995+ PyObject * mod = PyImport_ImportModule (module_name );
4996+ if (mod != NULL ) {
4997+ Py_DECREF (mod ); /* sys.modules holds the reference */
4998+ } else {
4999+ /* Clear error - import failure is not fatal */
5000+ PyErr_Clear ();
5001+ }
5002+
5003+ enif_free (module_name );
5004+ }
5005+
5006+ py_context_release (& guard );
5007+ return ATOM_OK ;
5008+ }
5009+
5010+ /**
5011+ * @brief Flush imports from an interpreter's sys.modules
5012+ *
5013+ * nif_interp_flush_imports(Ref, Modules) -> ok
5014+ *
5015+ * Removes specified modules from sys.modules and clears the per-context
5016+ * module_cache optimization layer.
5017+ *
5018+ * @param Ref Context reference
5019+ * @param Modules List of binary module names to remove from sys.modules
5020+ */
5021+ static ERL_NIF_TERM nif_interp_flush_imports (ErlNifEnv * env , int argc , const ERL_NIF_TERM argv []) {
5022+ (void )argc ;
5023+ py_context_t * ctx ;
5024+
5025+ if (!runtime_is_running ()) {
5026+ return ATOM_OK ; /* Nothing to flush if Python not running */
5027+ }
5028+
5029+ if (!enif_get_resource (env , argv [0 ], PY_CONTEXT_RESOURCE_TYPE , (void * * )& ctx )) {
5030+ return make_error (env , "invalid_context" );
5031+ }
5032+
5033+ if (ctx -> destroyed ) {
5034+ return ATOM_OK ; /* Already destroyed */
5035+ }
5036+
5037+ #ifdef HAVE_SUBINTERPRETERS
5038+ /* OWN_GIL mode: dispatch to the dedicated thread */
5039+ if (ctx -> uses_own_gil ) {
5040+ return dispatch_flush_imports_to_owngil (env , ctx , argv [1 ]);
5041+ }
5042+ #endif
5043+
5044+ py_context_guard_t guard = py_context_acquire (ctx );
5045+ if (!guard .acquired ) {
5046+ return ATOM_OK ; /* Can't acquire - just return ok */
5047+ }
5048+
5049+ /* Remove specified modules from sys.modules */
5050+ PyObject * sys_modules = PyImport_GetModuleDict (); /* Borrowed ref */
5051+ if (sys_modules != NULL ) {
5052+ ERL_NIF_TERM head , tail = argv [1 ];
5053+ while (enif_get_list_cell (env , tail , & head , & tail )) {
5054+ ErlNifBinary mod_bin ;
5055+ if (enif_inspect_binary (env , head , & mod_bin )) {
5056+ char * mod_name = binary_to_string (& mod_bin );
5057+ if (mod_name != NULL ) {
5058+ /* Remove module from sys.modules if present */
5059+ if (PyDict_GetItemString (sys_modules , mod_name ) != NULL ) {
5060+ PyDict_DelItemString (sys_modules , mod_name );
5061+ }
5062+ enif_free (mod_name );
5063+ }
5064+ }
5065+ }
5066+ }
5067+
5068+ /* Clear per-context optimization cache */
5069+ if (ctx -> module_cache != NULL ) {
5070+ PyDict_Clear (ctx -> module_cache );
5071+ }
5072+
5073+ py_context_release (& guard );
5074+ return ATOM_OK ;
5075+ }
5076+
47385077/**
47395078 * @brief Execute Python statements using a process-local environment
47405079 *
@@ -6792,12 +7131,6 @@ static ErlNifFunc nif_funcs[] = {
67927131 /* Per-process namespace NIFs */
67937132 {"event_loop_exec" , 2 , nif_event_loop_exec , ERL_NIF_DIRTY_JOB_IO_BOUND },
67947133 {"event_loop_eval" , 2 , nif_event_loop_eval , ERL_NIF_DIRTY_JOB_IO_BOUND },
6795- /* Module import caching NIFs */
6796- {"loop_import_module" , 2 , nif_loop_import_module , ERL_NIF_DIRTY_JOB_IO_BOUND },
6797- {"loop_import_function" , 3 , nif_loop_import_function , ERL_NIF_DIRTY_JOB_IO_BOUND },
6798- {"loop_flush_import_cache" , 1 , nif_loop_flush_import_cache , 0 },
6799- {"loop_import_stats" , 1 , nif_loop_import_stats , 0 },
6800- {"loop_import_list" , 1 , nif_loop_import_list , 0 },
68017134 {"add_reader" , 3 , nif_add_reader , 0 },
68027135 {"remove_reader" , 2 , nif_remove_reader , 0 },
68037136 {"add_writer" , 3 , nif_add_writer , 0 },
@@ -6870,6 +7203,8 @@ static ErlNifFunc nif_funcs[] = {
68707203 {"context_eval" , 4 , nif_context_eval_with_env , ERL_NIF_DIRTY_JOB_CPU_BOUND },
68717204 {"context_call" , 6 , nif_context_call_with_env , ERL_NIF_DIRTY_JOB_CPU_BOUND },
68727205 {"create_local_env" , 1 , nif_create_local_env , 0 },
7206+ {"interp_apply_imports" , 2 , nif_interp_apply_imports , ERL_NIF_DIRTY_JOB_CPU_BOUND },
7207+ {"interp_flush_imports" , 2 , nif_interp_flush_imports , ERL_NIF_DIRTY_JOB_CPU_BOUND },
68737208 {"context_call_method" , 4 , nif_context_call_method , ERL_NIF_DIRTY_JOB_CPU_BOUND },
68747209 {"context_to_term" , 1 , nif_context_to_term , 0 },
68757210 {"context_interp_id" , 1 , nif_context_interp_id , 0 },
0 commit comments