99//! native callback that feeds an async reader. The framing is unchanged — the
1010//! same LSP `Content-Length:` frames the stdio transport uses.
1111
12+ use std:: collections:: HashMap ;
1213use std:: ffi:: c_void;
1314use std:: path:: { Path , PathBuf } ;
1415use std:: pin:: Pin ;
1516use std:: sync:: Arc ;
1617use std:: sync:: atomic:: { AtomicBool , AtomicPtr , AtomicU32 , Ordering } ;
18+ use std:: sync:: { Mutex , OnceLock } ;
1719use std:: task:: { Context , Poll } ;
1820
1921use libloading:: Library ;
@@ -56,11 +58,11 @@ extern "C" fn on_outbound(user_data: *mut c_void, bytes: *const u8, len: usize)
5658 let _ = state. tx . send ( slice. to_vec ( ) ) ;
5759}
5860
59- /// Loaded library, bound exports, and connection lifecycle state, shared
60- /// between the [`FfiWriter`] and the owning [`Client`]. Kept alive for the
61- /// connection's lifetime; dropping it unloads the cdylib.
61+ /// Bound exports and connection lifecycle state, shared between the
62+ /// [`FfiWriter`] and the owning [`Client`]. The cdylib itself is loaded
63+ /// process-globally and never unloaded (see [`load_library`]), so this holds
64+ /// only the bound fn pointers and connection state.
6265pub ( crate ) struct FfiShared {
63- _lib : Library ,
6466 host_shutdown : HostShutdownFn ,
6567 connection_write : ConnectionWriteFn ,
6668 connection_close : ConnectionCloseFn ,
@@ -185,10 +187,10 @@ impl AsyncWrite for FfiWriter {
185187 }
186188}
187189
188- /// Prepared FFI host: the loaded cdylib plus the spawn arguments needed to
189- /// start the runtime worker.
190+ /// Prepared FFI host: the bound cdylib exports plus the spawn arguments needed
191+ /// to start the runtime worker. The cdylib is loaded process-globally and never
192+ /// unloaded (see [`load_library`]).
190193pub ( crate ) struct FfiHost {
191- lib : Library ,
192194 library_path : PathBuf ,
193195 entrypoint : PathBuf ,
194196 environment : Vec < ( String , String ) > ,
@@ -199,8 +201,8 @@ pub(crate) struct FfiHost {
199201 connection_close : ConnectionCloseFn ,
200202}
201203
202- // SAFETY: as for `FfiShared` — the bound exports are plain fn pointers and the
203- // library handle is safe to move to the blocking thread that starts the host.
204+ // SAFETY: as for `FfiShared` — the bound exports are plain fn pointers, safe to
205+ // move to the blocking thread that starts the host.
204206unsafe impl Send for FfiHost { }
205207
206208impl FfiHost {
@@ -215,29 +217,19 @@ impl FfiHost {
215217 environment : Vec < ( String , String ) > ,
216218 ) -> Result < Self , Error > {
217219 let library_path = resolve_library_path ( entrypoint) ?;
218- let lib = unsafe { Library :: new ( & library_path) } . map_err ( |e| {
219- Error :: with_message (
220- ErrorKind :: InvalidConfig ,
221- format ! (
222- "failed to load in-process runtime library '{}': {e}" ,
223- library_path. display( )
224- ) ,
225- )
226- } ) ?;
220+ let lib = load_library ( & library_path) ?;
227221
228- let host_start =
229- * bind :: < HostStartFn > ( & lib, b"copilot_runtime_host_start\0 " , & library_path) ?;
222+ let host_start = * bind :: < HostStartFn > ( lib, b"copilot_runtime_host_start\0 " , & library_path) ?;
230223 let host_shutdown =
231- * bind :: < HostShutdownFn > ( & lib, b"copilot_runtime_host_shutdown\0 " , & library_path) ?;
224+ * bind :: < HostShutdownFn > ( lib, b"copilot_runtime_host_shutdown\0 " , & library_path) ?;
232225 let connection_open =
233- * bind :: < ConnectionOpenFn > ( & lib, b"copilot_runtime_connection_open\0 " , & library_path) ?;
226+ * bind :: < ConnectionOpenFn > ( lib, b"copilot_runtime_connection_open\0 " , & library_path) ?;
234227 let connection_write =
235- * bind :: < ConnectionWriteFn > ( & lib, b"copilot_runtime_connection_write\0 " , & library_path) ?;
228+ * bind :: < ConnectionWriteFn > ( lib, b"copilot_runtime_connection_write\0 " , & library_path) ?;
236229 let connection_close =
237- * bind :: < ConnectionCloseFn > ( & lib, b"copilot_runtime_connection_close\0 " , & library_path) ?;
230+ * bind :: < ConnectionCloseFn > ( lib, b"copilot_runtime_connection_close\0 " , & library_path) ?;
238231
239232 Ok ( Self {
240- lib,
241233 library_path,
242234 entrypoint : entrypoint. to_path_buf ( ) ,
243235 environment,
@@ -310,7 +302,6 @@ impl FfiHost {
310302 }
311303
312304 let shared = Arc :: new ( FfiShared {
313- _lib : self . lib ,
314305 host_shutdown : self . host_shutdown ,
315306 connection_write : self . connection_write ,
316307 connection_close : self . connection_close ,
@@ -355,6 +346,44 @@ fn bind<'lib, T>(
355346 } )
356347}
357348
349+ /// Loads the runtime cdylib once per process and never unloads it, returning a
350+ /// `'static` reference. Subsequent loads of the same path reuse the first
351+ /// handle.
352+ ///
353+ /// The library is intentionally leaked (never `FreeLibrary`/`dlclose`d), so its
354+ /// code stays mapped for the process lifetime. This mirrors the Node host
355+ /// (which loads the cdylib once into a module-global and never unloads it) and
356+ /// the runtime's own process-global tokio runtime that is never shut down.
357+ /// Unloading the cdylib while shutting a connection down races the runtime's
358+ /// worker threads: on Windows, `FreeLibrary` unmaps the code and any late
359+ /// worker-thread callback into it faults (`STATUS_ACCESS_VIOLATION`). Keeping
360+ /// the module mapped avoids that while `close()` still tears the host down.
361+ fn load_library ( library_path : & Path ) -> Result < & ' static Library , Error > {
362+ static LIBRARIES : OnceLock < Mutex < HashMap < PathBuf , & ' static Library > > > = OnceLock :: new ( ) ;
363+ let cache = LIBRARIES . get_or_init ( || Mutex :: new ( HashMap :: new ( ) ) ) ;
364+
365+ let mut guard = cache
366+ . lock ( )
367+ . unwrap_or_else ( |poisoned| poisoned. into_inner ( ) ) ;
368+ if let Some ( lib) = guard. get ( library_path) {
369+ return Ok ( * lib) ;
370+ }
371+
372+ let lib = unsafe { Library :: new ( library_path) } . map_err ( |e| {
373+ Error :: with_message (
374+ ErrorKind :: InvalidConfig ,
375+ format ! (
376+ "failed to load in-process runtime library '{}': {e}" ,
377+ library_path. display( )
378+ ) ,
379+ )
380+ } ) ?;
381+ // Leak the library so it is never unloaded for the process lifetime.
382+ let leaked: & ' static Library = Box :: leak ( Box :: new ( lib) ) ;
383+ guard. insert ( library_path. to_path_buf ( ) , leaked) ;
384+ Ok ( leaked)
385+ }
386+
358387/// The natural platform shared-library file name for the runtime cdylib — the
359388/// `.node` file renamed to what the Rust cdylib would be called on this OS.
360389fn natural_library_name ( ) -> & ' static str {
0 commit comments