1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use crate::Router;
use crate::server::server_middleware::{
hmr_client_script, inject_hmr_script, zstd_static_middleware,
};
#[cfg(target_os = "windows")]
fn is_expected_loaded_library_removal_error(error: &std::io::Error) -> bool {
// Windows may report either ERROR_ACCESS_DENIED or
// ERROR_SHARING_VIOLATION while the successfully loaded image remains
// mapped. Stale copies are retried by the cleanup at the next startup.
matches!(error.raw_os_error(), Some(5) | Some(32))
}
/// Dynamically loads an application router from a compiled dynamic library (`cdylib`).
#[cfg_attr(mutants, mutants::skip)]
#[allow(unsafe_code)]
pub fn load_dylib_router(
lib_path: &str,
is_dev: bool,
) -> Result<(axum::Router, libloading::Library), Box<dyn std::error::Error>> {
let lib_extension = if cfg!(target_os = "windows") {
"dll"
} else if cfg!(target_os = "macos") {
"dylib"
} else {
"so"
};
let full_lib_path = if lib_path.ends_with(".dll")
|| lib_path.ends_with(".so")
|| lib_path.ends_with(".dylib")
{
lib_path.to_string()
} else {
format!("{}.{}", lib_path, lib_extension)
};
let path_buf = std::path::Path::new(&full_lib_path);
if !path_buf.exists() {
return Err(format!("Dylib not found at: {}", full_lib_path).into());
}
let parent = path_buf.parent().unwrap_or(std::path::Path::new("."));
let filename = path_buf
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid dylib path or non-UTF-8 characters detected",
)
})?;
// Clean up older active files that are no longer locked by any process
let expected_prefix = format!("{}_active_", filename);
if let Ok(entries) = std::fs::read_dir(parent) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with(&expected_prefix) && name.ends_with(lib_extension) {
let _ = std::fs::remove_file(path);
}
}
}
}
// L-1: UUID v4 avoids clock-resolution collisions between temporary names.
let unique_id = uuid::Uuid::new_v4().as_simple().to_string();
let temp_filename = format!("{}_active_{}.{}", filename, unique_id, lib_extension);
let temp_path = parent.join(temp_filename);
// Copy the dylib file to prevent locking issues
std::fs::copy(&full_lib_path, &temp_path)?;
// SAFETY: The code below performs dynamic library loading and raw pointer extraction.
// Invariant requirements for safety:
// - The dynamically loaded library must expose a symbol `rullst_router_init`
// with signature `unsafe extern "C" fn() -> *mut Router`.
// - The `rullst_router_init` function must allocate a `Box<Router>` and return
// the raw pointer produced by `Box::into_raw`. The caller here uses
// `Box::from_raw` to take ownership of that pointer; therefore the
// library MUST NOT keep or free that pointer after returning it.
// - The loaded library must be ABI-compatible with the host Router layout.
// - The `temp_path` copy must remain available on disk for the lifetime of
// any references into the loaded library. We keep the returned `Library`
// object to ensure the library remains mapped until dropped by the caller.
// - Calls into the plugin must be synchronized appropriately by the host if
// the plugin is not internally thread-safe.
//
// This block is `unsafe` because it relies on the above invariants; any
// future changes to router ABI or plugin implementations must be reflected
// here and documented. Review and audit this section when upgrading
// `libloading`, `Router` types, or changing the plugin API.
// SAFETY: `temp_path` names the freshly copied application library. The
// returned handle is retained for at least as long as every value obtained
// from the library, so none of its code is unloaded while it can be called.
let lib = unsafe { libloading::Library::new(&temp_path)? };
if let Err(e) = std::fs::remove_file(&temp_path) {
#[cfg(not(target_os = "windows"))]
eprintln!(
"⚠️ Rullst: failed to remove temporary dylib file at {:?}: {}",
temp_path, e
);
#[cfg(target_os = "windows")]
{
if !is_expected_loaded_library_removal_error(&e) {
eprintln!(
"⚠️ Rullst: failed to remove temporary dylib file at {:?}: {}",
temp_path, e
);
}
}
}
// SAFETY: the development cdylib contract requires this exact symbol and
// signature, built by the same Rullst/Rust toolchain as the host process.
let init_fn: libloading::Symbol<unsafe extern "C" fn() -> *mut Router> =
unsafe { lib.get(b"rullst_router_init")? };
// SAFETY: calling the foreign initializer is covered by the contract above.
let router_ptr = unsafe { init_fn() };
let router_ptr = std::ptr::NonNull::new(router_ptr).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"rullst_router_init returned a null Router pointer",
)
})?;
// SAFETY: the non-null pointer was produced by `Box::into_raw` in the
// initializer, has not been freed or aliased for ownership, and is consumed
// exactly once here.
let rullst_router = unsafe { *Box::from_raw(router_ptr.as_ptr()) };
// Convert the application-owned Rullst router to Axum. Optional surfaces
// such as Scalar docs must be mounted explicitly so application routes are
// never shadowed or duplicated by the server runtime.
let mut axum_router = rullst_router.into_axum();
// Serve static files from "static" directory if it exists
if std::path::Path::new("static").exists() {
axum_router = axum_router
.nest_service(
"/static",
tower_http::services::ServeDir::new("static").precompressed_br(),
)
.layer(axum::middleware::from_fn(zstd_static_middleware));
}
// Attach development explain / console routes
if is_dev {
axum_router = axum_router
.route(
"/_rullst/explain",
axum::routing::get(crate::error_console::handle_explain),
)
.route(
"/_rullst/autofix",
axum::routing::post(crate::error_console::handle_autofix),
)
.route(
"/_rullst/hmr-client.js",
axum::routing::get(hmr_client_script),
)
.layer(axum::middleware::from_fn(
crate::error_console::catch_panic_middleware,
))
.layer(axum::middleware::from_fn(inject_hmr_script));
}
Ok((axum_router, lib))
}
#[cfg(test)]
#[path = "dylib_loader_tests.rs"]
mod tests;