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