Skip to content

Commit fdc5fd9

Browse files
authored
fix: 🐛 fix crash in multiCompiler (#2744)
* fix: 🐛 fix crash in multiCompiler * chore: add safety comment and enable test:example
1 parent 5c48310 commit fdc5fd9

File tree

5 files changed

+81
-16
lines changed

5 files changed

+81
-16
lines changed

.changeset/metal-dodos-play.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rspack/binding": patch
3+
"@rspack/core": patch
4+
---
5+
6+
fix: fix crash in multiCompiler

.github/actions/docker-test/action.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ runs:
4242
pnpm install
4343
pnpm run build:js
4444
pnpm run test:unit
45-
# These two breaks on many targets, diabled for now
46-
# pnpm run test:example
45+
pnpm run test:example
4746
# e2e is disabled because this does not run on some targets
4847
# pnpm run test:e2e

.github/workflows/reusable-build.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,7 @@ jobs:
279279
pnpm install
280280
pnpm run build:js
281281
pnpm run test:unit
282-
# These two breaks on many targets, diabled for now
283-
# pnpm run test:example
282+
pnpm run test:example
284283
# pnpm run test:e2e
285284
286285
### x86_64-pc-windows-msvc
@@ -303,6 +302,5 @@ jobs:
303302
pnpm install
304303
pnpm run build:js
305304
pnpm run test:unit
306-
# These two breaks on many targets, diabled for now
307-
# pnpm run test:example
305+
pnpm run test:example
308306
# pnpm run test:e2e

crates/node_binding/src/lib.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ extern crate napi_derive;
77
extern crate rspack_binding_macros;
88

99
use std::collections::HashSet;
10+
use std::pin::Pin;
1011
use std::sync::atomic::{AtomicU32, Ordering};
1112

1213
use dashmap::DashMap;
@@ -52,7 +53,7 @@ where
5253
))
5354
})?;
5455

55-
f(&mut *inner)
56+
f(&mut inner)
5657
}
5758

5859
/// Acquire a shared reference to the inner hashmap.
@@ -112,7 +113,7 @@ unsafe impl<K, V> Send for SingleThreadedHashMap<K, V> {}
112113
unsafe impl<K, V> Sync for SingleThreadedHashMap<K, V> {}
113114

114115
static COMPILERS: Lazy<
115-
SingleThreadedHashMap<CompilerId, rspack_core::Compiler<AsyncNodeWritableFileSystem>>,
116+
SingleThreadedHashMap<CompilerId, Pin<Box<rspack_core::Compiler<AsyncNodeWritableFileSystem>>>>,
116117
> = Lazy::new(Default::default);
117118

118119
static COMPILER_ID: AtomicU32 = AtomicU32::new(1);
@@ -159,7 +160,7 @@ impl Rspack {
159160
);
160161

161162
let id = COMPILER_ID.fetch_add(1, Ordering::SeqCst);
162-
unsafe { COMPILERS.insert_if_vacant(id, rspack) }?;
163+
unsafe { COMPILERS.insert_if_vacant(id, Box::pin(rspack)) }?;
163164

164165
Ok(Self { id, disabled_hooks })
165166
}
@@ -185,9 +186,9 @@ impl Rspack {
185186
ts_args_type = "callback: (err: null | Error) => void"
186187
)]
187188
pub fn build(&self, env: Env, f: JsFunction) -> Result<()> {
188-
let handle_build = |compiler: &mut _| {
189+
let handle_build = |compiler: &mut Pin<Box<rspack_core::Compiler<_>>>| {
189190
// Safety: compiler is stored in a global hashmap, so it's guaranteed to be alive.
190-
let compiler: &'static mut rspack_core::Compiler<AsyncNodeWritableFileSystem> =
191+
let compiler: &'static mut Pin<Box<rspack_core::Compiler<AsyncNodeWritableFileSystem>>> =
191192
unsafe { std::mem::transmute::<&'_ mut _, &'static mut _>(compiler) };
192193

193194
callbackify(env, f, async move {
@@ -218,9 +219,12 @@ impl Rspack {
218219
removed_files: Vec<String>,
219220
f: JsFunction,
220221
) -> Result<()> {
221-
let handle_rebuild = |compiler: &mut _| {
222+
let handle_rebuild = |compiler: &mut Pin<Box<rspack_core::Compiler<_>>>| {
222223
// Safety: compiler is stored in a global hashmap, so it's guaranteed to be alive.
223-
let compiler: &'static mut rspack_core::Compiler<AsyncNodeWritableFileSystem> =
224+
// The reason why use Box<Compiler> here instead of Compiler itself is that:
225+
// Compilers may expand and change its layout underneath, make Compiler layout change.
226+
// Use Box to make sure the Compiler layout won't change
227+
let compiler: &'static mut Pin<Box<rspack_core::Compiler<AsyncNodeWritableFileSystem>>> =
224228
unsafe { std::mem::transmute::<&'_ mut _, &'static mut _>(compiler) };
225229

226230
callbackify(env, f, async move {
@@ -248,9 +252,12 @@ impl Rspack {
248252
/// **Note** that this method is not safe if you cache the _JsCompilation_ on the Node side, as it will be invalidated by the next build and accessing a dangling ptr is a UB.
249253
#[napi(catch_unwind, js_name = "unsafe_last_compilation")]
250254
pub fn unsafe_last_compilation<F: Fn(JsCompilation) -> Result<()>>(&self, f: F) -> Result<()> {
251-
let handle_last_compilation = |compiler: &mut _| {
255+
let handle_last_compilation = |compiler: &mut Pin<Box<rspack_core::Compiler<_>>>| {
252256
// Safety: compiler is stored in a global hashmap, and compilation is only available in the callback of this function, so it is safe to cast to a static lifetime. See more in the warning part of this method.
253-
let compiler: &'static mut rspack_core::Compiler<AsyncNodeWritableFileSystem> =
257+
// The reason why use Box<Compiler> here instead of Compiler itself is that:
258+
// Compilers may expand and change its layout underneath, make Compiler layout change.
259+
// Use Box to make sure the Compiler layout won't change
260+
let compiler: &'static mut Pin<Box<rspack_core::Compiler<AsyncNodeWritableFileSystem>>> =
254261
unsafe { std::mem::transmute::<&'_ mut _, &'static mut _>(compiler) };
255262
f(JsCompilation::from_compilation(&mut compiler.compilation))
256263
};

packages/rspack/tests/MultiCompiler.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from "path";
22
import { createFsFromVolume, Volume } from "memfs";
33
import { FileSystemInfoEntry, Watcher } from "../src/util/fs";
4-
import { MultiRspackOptions, rspack, RspackOptions } from "../src";
4+
import { Compiler, MultiRspackOptions, rspack, RspackOptions } from "../src";
55
import { assert } from "console";
66

77
const createMultiCompiler = (
@@ -672,3 +672,58 @@ describe("MultiCompiler", function () {
672672
});
673673
}, 20000);
674674
});
675+
676+
describe("Pressure test", function () {
677+
it("should work well in multiCompilers", done => {
678+
const configs = Array(100).fill({
679+
context: path.join(__dirname, "fixtures"),
680+
entry: "./a.js"
681+
});
682+
683+
const multiCompiler = rspack(configs);
684+
685+
multiCompiler.run(err => {
686+
if (err) done(err);
687+
else done();
688+
});
689+
});
690+
691+
it("should work well in concurrent", async () => {
692+
const total = 100;
693+
694+
let finish = 0;
695+
696+
const runnings: Promise<null>[] = [];
697+
698+
for (let i = 0; i < total; i++) {
699+
if (i % 10 == 0) {
700+
// Insert new instance while we are running
701+
rspack(
702+
{
703+
context: path.join(__dirname, "fixtures"),
704+
entry: "./a.js"
705+
},
706+
() => {}
707+
);
708+
}
709+
710+
runnings.push(
711+
new Promise(resolve => {
712+
rspack(
713+
{
714+
context: path.join(__dirname, "fixtures"),
715+
entry: "./a.js"
716+
},
717+
err => {
718+
resolve(null);
719+
if (!err) finish++;
720+
}
721+
);
722+
})
723+
);
724+
}
725+
726+
await Promise.all(runnings);
727+
expect(finish).toBe(total);
728+
});
729+
});

0 commit comments

Comments
 (0)