@@ -3224,6 +3224,77 @@ static void owngil_execute_apply_imports(py_context_t *ctx) {
32243224 ctx -> response_ok = true;
32253225}
32263226
3227+ /**
3228+ * @brief Apply paths to sys.path in OWN_GIL context
3229+ *
3230+ * Paths are inserted at the beginning of sys.path.
3231+ */
3232+ static void owngil_execute_apply_paths (py_context_t * ctx ) {
3233+ /* Get sys.path */
3234+ PyObject * sys_module = PyImport_ImportModule ("sys" );
3235+ if (sys_module == NULL ) {
3236+ PyErr_Clear ();
3237+ ctx -> response_term = enif_make_tuple2 (ctx -> shared_env ,
3238+ enif_make_atom (ctx -> shared_env , "error" ),
3239+ enif_make_atom (ctx -> shared_env , "sys_import_failed" ));
3240+ ctx -> response_ok = false;
3241+ return ;
3242+ }
3243+
3244+ PyObject * sys_path = PyObject_GetAttrString (sys_module , "path" );
3245+ Py_DECREF (sys_module );
3246+ if (sys_path == NULL || !PyList_Check (sys_path )) {
3247+ Py_XDECREF (sys_path );
3248+ PyErr_Clear ();
3249+ ctx -> response_term = enif_make_tuple2 (ctx -> shared_env ,
3250+ enif_make_atom (ctx -> shared_env , "error" ),
3251+ enif_make_atom (ctx -> shared_env , "sys_path_not_list" ));
3252+ ctx -> response_ok = false;
3253+ return ;
3254+ }
3255+
3256+ /* Count paths first */
3257+ ERL_NIF_TERM head , tail = ctx -> request_term ;
3258+ int path_count = 0 ;
3259+ while (enif_get_list_cell (ctx -> shared_env , tail , & head , & tail )) {
3260+ path_count ++ ;
3261+ }
3262+
3263+ /* Insert in reverse order so first path ends up first */
3264+ for (int i = 0 ; i < path_count ; i ++ ) {
3265+ /* Skip to the i-th element from the end */
3266+ ERL_NIF_TERM current = ctx -> request_term ;
3267+ for (int j = 0 ; j < path_count - 1 - i ; j ++ ) {
3268+ enif_get_list_cell (ctx -> shared_env , current , & head , & current );
3269+ }
3270+ enif_get_list_cell (ctx -> shared_env , current , & head , & current );
3271+
3272+ ErlNifBinary path_bin ;
3273+ if (!enif_inspect_binary (ctx -> shared_env , head , & path_bin )) {
3274+ continue ;
3275+ }
3276+
3277+ /* Convert to Python string */
3278+ PyObject * path_str = PyUnicode_FromStringAndSize ((char * )path_bin .data , path_bin .size );
3279+ if (path_str == NULL ) {
3280+ PyErr_Clear ();
3281+ continue ;
3282+ }
3283+
3284+ /* Check if already in sys.path */
3285+ int already_present = PySequence_Contains (sys_path , path_str );
3286+ if (already_present <= 0 ) {
3287+ /* Insert at position 0 */
3288+ PyList_Insert (sys_path , 0 , path_str );
3289+ }
3290+ Py_DECREF (path_str );
3291+ }
3292+
3293+ Py_DECREF (sys_path );
3294+ ctx -> response_term = enif_make_atom (ctx -> shared_env , "ok" );
3295+ ctx -> response_ok = true;
3296+ }
3297+
32273298/**
32283299 * @brief Execute a request based on its type
32293300 */
@@ -3262,6 +3333,9 @@ static void owngil_execute_request(py_context_t *ctx) {
32623333 case CTX_REQ_APPLY_IMPORTS :
32633334 owngil_execute_apply_imports (ctx );
32643335 break ;
3336+ case CTX_REQ_APPLY_PATHS :
3337+ owngil_execute_apply_paths (ctx );
3338+ break ;
32653339 default :
32663340 ctx -> response_term = enif_make_tuple2 (ctx -> shared_env ,
32673341 enif_make_atom (ctx -> shared_env , "error" ),
@@ -3871,6 +3945,50 @@ static ERL_NIF_TERM dispatch_apply_imports_to_owngil(
38713945 return result ;
38723946}
38733947
3948+ /**
3949+ * @brief Dispatch apply_paths request to OWN_GIL worker thread
3950+ *
3951+ * @param env Current NIF environment
3952+ * @param ctx OWN_GIL context
3953+ * @param paths_term List of path binaries
3954+ * @return ok | {error, Reason}
3955+ */
3956+ static ERL_NIF_TERM dispatch_apply_paths_to_owngil (
3957+ ErlNifEnv * env , py_context_t * ctx , ERL_NIF_TERM paths_term
3958+ ) {
3959+ if (!atomic_load (& ctx -> thread_running )) {
3960+ return make_error (env , "thread_not_running" );
3961+ }
3962+
3963+ pthread_mutex_lock (& ctx -> request_mutex );
3964+
3965+ enif_clear_env (ctx -> shared_env );
3966+ ctx -> request_term = enif_make_copy (ctx -> shared_env , paths_term );
3967+ ctx -> request_type = CTX_REQ_APPLY_PATHS ;
3968+
3969+ pthread_cond_signal (& ctx -> request_ready );
3970+
3971+ /* Wait for response with timeout */
3972+ struct timespec deadline ;
3973+ clock_gettime (CLOCK_REALTIME , & deadline );
3974+ deadline .tv_sec += OWNGIL_DISPATCH_TIMEOUT_SECS ;
3975+
3976+ while (ctx -> request_type != CTX_REQ_NONE ) {
3977+ int rc = pthread_cond_timedwait (& ctx -> response_ready , & ctx -> request_mutex , & deadline );
3978+ if (rc == ETIMEDOUT ) {
3979+ atomic_store (& ctx -> thread_running , false);
3980+ pthread_mutex_unlock (& ctx -> request_mutex );
3981+ fprintf (stderr , "OWN_GIL apply_paths dispatch timeout: worker thread unresponsive\n" );
3982+ return make_error (env , "worker_timeout" );
3983+ }
3984+ }
3985+
3986+ ERL_NIF_TERM result = enif_make_copy (env , ctx -> response_term );
3987+ pthread_mutex_unlock (& ctx -> request_mutex );
3988+
3989+ return result ;
3990+ }
3991+
38743992#endif /* HAVE_SUBINTERPRETERS */
38753993
38763994/**
@@ -4935,6 +5053,104 @@ static ERL_NIF_TERM nif_interp_apply_imports(ErlNifEnv *env, int argc, const ERL
49355053 return ATOM_OK ;
49365054}
49375055
5056+ /**
5057+ * @brief Apply a list of paths to an interpreter's sys.path
5058+ *
5059+ * nif_interp_apply_paths(Ref, Paths) -> ok | {error, Reason}
5060+ *
5061+ * Paths: [PathBin, ...]
5062+ * Inserts paths at the beginning of sys.path so they take precedence.
5063+ */
5064+ static ERL_NIF_TERM nif_interp_apply_paths (ErlNifEnv * env , int argc , const ERL_NIF_TERM argv []) {
5065+ (void )argc ;
5066+ py_context_t * ctx ;
5067+
5068+ if (!runtime_is_running ()) {
5069+ return make_error (env , "python_not_running" );
5070+ }
5071+
5072+ if (!enif_get_resource (env , argv [0 ], PY_CONTEXT_RESOURCE_TYPE , (void * * )& ctx )) {
5073+ return make_error (env , "invalid_context" );
5074+ }
5075+
5076+ if (ctx -> destroyed ) {
5077+ return make_error (env , "context_destroyed" );
5078+ }
5079+
5080+ #ifdef HAVE_SUBINTERPRETERS
5081+ /* OWN_GIL mode: dispatch to the dedicated thread */
5082+ if (ctx -> uses_own_gil ) {
5083+ return dispatch_apply_paths_to_owngil (env , ctx , argv [1 ]);
5084+ }
5085+ #endif
5086+
5087+ py_context_guard_t guard = py_context_acquire (ctx );
5088+ if (!guard .acquired ) {
5089+ return make_error (env , "acquire_failed" );
5090+ }
5091+
5092+ /* Get sys.path */
5093+ PyObject * sys_module = PyImport_ImportModule ("sys" );
5094+ if (sys_module == NULL ) {
5095+ py_context_release (& guard );
5096+ return make_error (env , "sys_import_failed" );
5097+ }
5098+
5099+ PyObject * sys_path = PyObject_GetAttrString (sys_module , "path" );
5100+ Py_DECREF (sys_module );
5101+ if (sys_path == NULL || !PyList_Check (sys_path )) {
5102+ Py_XDECREF (sys_path );
5103+ py_context_release (& guard );
5104+ return make_error (env , "sys_path_not_list" );
5105+ }
5106+
5107+ /* Process each path - insert at beginning in reverse order */
5108+ /* First, collect all paths */
5109+ ERL_NIF_TERM head , tail = argv [1 ];
5110+ int path_count = 0 ;
5111+ ERL_NIF_TERM paths_list = argv [1 ];
5112+
5113+ /* Count paths */
5114+ while (enif_get_list_cell (env , tail , & head , & tail )) {
5115+ path_count ++ ;
5116+ }
5117+
5118+ /* Insert in reverse order so first path ends up first */
5119+ tail = paths_list ;
5120+ for (int i = 0 ; i < path_count ; i ++ ) {
5121+ /* Skip to the i-th element from the end */
5122+ ERL_NIF_TERM current = paths_list ;
5123+ for (int j = 0 ; j < path_count - 1 - i ; j ++ ) {
5124+ enif_get_list_cell (env , current , & head , & current );
5125+ }
5126+ enif_get_list_cell (env , current , & head , & current );
5127+
5128+ ErlNifBinary path_bin ;
5129+ if (!enif_inspect_binary (env , head , & path_bin )) {
5130+ continue ;
5131+ }
5132+
5133+ /* Convert to Python string */
5134+ PyObject * path_str = PyUnicode_FromStringAndSize ((char * )path_bin .data , path_bin .size );
5135+ if (path_str == NULL ) {
5136+ PyErr_Clear ();
5137+ continue ;
5138+ }
5139+
5140+ /* Check if already in sys.path */
5141+ int already_present = PySequence_Contains (sys_path , path_str );
5142+ if (already_present <= 0 ) {
5143+ /* Insert at position 0 */
5144+ PyList_Insert (sys_path , 0 , path_str );
5145+ }
5146+ Py_DECREF (path_str );
5147+ }
5148+
5149+ Py_DECREF (sys_path );
5150+ py_context_release (& guard );
5151+ return ATOM_OK ;
5152+ }
5153+
49385154/**
49395155 * @brief Execute Python statements using a process-local environment
49405156 *
@@ -7065,6 +7281,7 @@ static ErlNifFunc nif_funcs[] = {
70657281 {"context_call" , 6 , nif_context_call_with_env , ERL_NIF_DIRTY_JOB_CPU_BOUND },
70667282 {"create_local_env" , 1 , nif_create_local_env , 0 },
70677283 {"interp_apply_imports" , 2 , nif_interp_apply_imports , ERL_NIF_DIRTY_JOB_CPU_BOUND },
7284+ {"interp_apply_paths" , 2 , nif_interp_apply_paths , ERL_NIF_DIRTY_JOB_CPU_BOUND },
70687285 {"context_call_method" , 4 , nif_context_call_method , ERL_NIF_DIRTY_JOB_CPU_BOUND },
70697286 {"context_to_term" , 1 , nif_context_to_term , 0 },
70707287 {"context_interp_id" , 1 , nif_context_interp_id , 0 },
0 commit comments