@@ -28,6 +28,7 @@ use alloc::format;
2828use alloc:: rc:: Rc ;
2929use alloc:: string:: { String , ToString } ;
3030use alloc:: vec:: Vec ;
31+ use core:: cell:: RefCell ;
3132
3233use anyhow:: { anyhow, Context as _} ;
3334use hashbrown:: HashMap ;
@@ -49,22 +50,68 @@ struct Handler<'a> {
4950 func : Persistent < Function < ' a > > ,
5051}
5152
53+ /// A module loader for user-registered modules.
54+ ///
55+ /// Stores module source code keyed by qualified name (e.g. `user:utils`).
56+ /// Modules are compiled lazily when first imported — this avoids ordering
57+ /// issues between modules that depend on each other.
58+ ///
59+ /// Implements both [`Resolver`] and [`Loader`] so it can be inserted into
60+ /// the rquickjs module loader chain alongside the host and native loaders.
61+ #[ derive( Default , Clone ) ]
62+ struct UserModuleLoader {
63+ modules : Rc < RefCell < HashMap < String , String > > > ,
64+ }
65+
66+ impl Resolver for UserModuleLoader {
67+ fn resolve ( & mut self , _ctx : & Ctx < ' _ > , base : & str , name : & str ) -> Result < String > {
68+ if self . modules . borrow ( ) . contains_key ( name) {
69+ Ok ( name. to_string ( ) )
70+ } else {
71+ Err ( rquickjs:: Error :: new_resolving ( base, name) )
72+ }
73+ }
74+ }
75+
76+ impl Loader for UserModuleLoader {
77+ fn load < ' js > ( & mut self , ctx : & Ctx < ' js > , name : & str ) -> Result < Module < ' js > > {
78+ let source = self
79+ . modules
80+ . borrow ( )
81+ . get ( name)
82+ . cloned ( )
83+ . ok_or_else ( || rquickjs:: Error :: new_loading ( name) ) ?;
84+ Module :: declare ( ctx. clone ( ) , name, source)
85+ }
86+ }
87+
5288/// This is the main entry point for the library.
5389/// It manages the QuickJS runtime, as well as the registered handlers and host modules.
5490pub struct JsRuntime {
5591 context : Context ,
5692 handlers : HashMap < String , Handler < ' static > > ,
93+ /// Lazily-loaded user modules, keyed by qualified name (e.g. `user:utils`).
94+ user_modules : UserModuleLoader ,
5795}
5896
5997// SAFETY:
6098// This is safe. The reason it is not automatically implemented by the compiler
61- // is because `rquickjs::Context` is not `Send` because it holds a raw pointer.
99+ // is because `rquickjs::Context` is not `Send` (it holds a raw pointer) and
100+ // `UserModuleLoader` contains `Rc<RefCell<HashMap>>` which is `!Send`.
101+ //
62102// Raw pointers in rust are not marked as `Send` as lint rather than an actual
63103// safety concern (see https://doc.rust-lang.org/nomicon/send-and-sync.html).
64104// Moreover, rquickjs DOES implement Send for Context when the "parallel" feature
65105// is enabled, further indicating that it is safe for this to implement `Send`.
66- // Moreover, every public method of `JsRuntime` takes `&mut self`, and so we can
67- // be certain that there are no concurrent accesses to it.
106+ //
107+ // The `Rc<RefCell<>>` in `UserModuleLoader` is shared with the rquickjs loader
108+ // chain (cloned during `set_loader`). This is safe because:
109+ // 1. Every public method of `JsRuntime` takes `&mut self`, ensuring exclusive access.
110+ // 2. The guest runtime is single-threaded (`#![no_std]` micro-VM).
111+ // 3. The `Rc` clone only creates shared ownership within the same thread.
112+ //
113+ // If the runtime ever becomes multi-threaded, `Rc<RefCell<>>` would need to be
114+ // replaced with `Arc<Mutex<>>` or similar.
68115unsafe impl Send for JsRuntime { }
69116
70117impl JsRuntime {
@@ -79,10 +126,25 @@ impl JsRuntime {
79126 // We need to do this before setting up the globals as many of the globals are implemented
80127 // as native modules, and so they need the module loader to be able to be loaded.
81128 let host_loader = HostModuleLoader :: default ( ) ;
129+ let user_modules = UserModuleLoader :: default ( ) ;
82130 let native_loader = NativeModuleLoader ;
83131 let module_loader = ModuleLoader :: new ( host) ;
84132
85- let loader = ( host_loader. clone ( ) , native_loader, module_loader) ;
133+ // User modules are second in the chain — after host modules but before
134+ // native and filesystem loaders — so `user:X` is resolved before falling
135+ // through to built-in or file-based resolution.
136+ //
137+ // NOTE: This means a user module with a qualified name matching a native
138+ // module (e.g. `"crypto"`) would shadow the built-in. In practice this
139+ // cannot happen accidentally because the host layer enforces the
140+ // `namespace:name` format (e.g. `"user:crypto"`), which never collides
141+ // with unqualified native module names.
142+ let loader = (
143+ host_loader. clone ( ) ,
144+ user_modules. clone ( ) ,
145+ native_loader,
146+ module_loader,
147+ ) ;
86148 runtime. set_loader ( loader. clone ( ) , loader) ;
87149
88150 context. with ( |ctx| -> anyhow:: Result < ( ) > {
@@ -97,6 +159,7 @@ impl JsRuntime {
97159 Ok ( Self {
98160 context,
99161 handlers : HashMap :: new ( ) ,
162+ user_modules,
100163 } )
101164 }
102165
@@ -201,6 +264,37 @@ impl JsRuntime {
201264 Ok ( ( ) )
202265 }
203266
267+ /// Register a user module with the runtime.
268+ ///
269+ /// The module source is stored for lazy compilation — it will be compiled
270+ /// and evaluated by QuickJS the first time it is imported by a handler or
271+ /// another user module. This avoids ordering issues between interdependent
272+ /// modules.
273+ ///
274+ /// The `module_name` should be the fully qualified name (e.g. `user:utils`)
275+ /// that guest JavaScript will use in `import` statements.
276+ ///
277+ /// # Validation
278+ ///
279+ /// The name must not be empty. Primary validation (colons, reserved
280+ /// namespaces, duplicates) is enforced by the host-side `JSSandbox` layer;
281+ /// this check provides defense-in-depth at the guest boundary.
282+ pub fn register_module (
283+ & mut self ,
284+ module_name : impl Into < String > ,
285+ module_source : impl Into < String > ,
286+ ) -> anyhow:: Result < ( ) > {
287+ let module_name = module_name. into ( ) ;
288+ if module_name. is_empty ( ) {
289+ anyhow:: bail!( "Module name must not be empty" ) ;
290+ }
291+ self . user_modules
292+ . modules
293+ . borrow_mut ( )
294+ . insert ( module_name, module_source. into ( ) ) ;
295+ Ok ( ( ) )
296+ }
297+
204298 /// Run a registered handler function with the given event data.
205299 /// The event data is passed as a JSON string, and the handler function is expected to return a value that can be serialized to JSON.
206300 /// The result is returned as a JSON string.
0 commit comments