Skip to content

Commit 57c093f

Browse files
committed
feat(napi/oxlint): pass AST in buffer to JS
1 parent 85db259 commit 57c093f

File tree

30 files changed

+371
-119
lines changed

30 files changed

+371
-119
lines changed

.github/generated/ast_changes_watch_list.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ src:
6363
- 'crates/oxc_syntax/src/serialize.rs'
6464
- 'crates/oxc_syntax/src/symbol.rs'
6565
- 'crates/oxc_traverse/src/generated/scopes_collector.rs'
66+
- 'napi/oxlint2/src/generated/constants.cjs'
67+
- 'napi/oxlint2/src/generated/raw_transfer_constants.rs'
6668
- 'napi/parser/generated/constants.js'
6769
- 'napi/parser/generated/deserialize/js.js'
6870
- 'napi/parser/generated/deserialize/ts.js'

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_allocator/src/generated/assert_layouts.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,30 @@ use crate::*;
99

1010
#[cfg(target_pointer_width = "64")]
1111
const _: () = {
12-
// Padding: 6 bytes
12+
// Padding: 2 bytes
1313
assert!(size_of::<RawTransferMetadata2>() == 32);
1414
assert!(align_of::<RawTransferMetadata2>() == 8);
1515
assert!(offset_of!(RawTransferMetadata2, data_offset) == 16);
16-
assert!(offset_of!(RawTransferMetadata2, is_ts) == 24);
17-
assert!(offset_of!(RawTransferMetadata2, id) == 20);
18-
assert!(offset_of!(RawTransferMetadata2, can_be_freed) == 25);
19-
assert!(offset_of!(RawTransferMetadata2, alloc_ptr) == 8);
20-
assert!(offset_of!(RawTransferMetadata2, _padding) == 0);
16+
assert!(offset_of!(RawTransferMetadata2, is_ts) == 28);
17+
assert!(offset_of!(RawTransferMetadata2, source_len) == 20);
18+
assert!(offset_of!(RawTransferMetadata2, id) == 24);
19+
assert!(offset_of!(RawTransferMetadata2, can_be_freed) == 29);
20+
assert!(offset_of!(RawTransferMetadata2, alloc_ptr) == 0);
21+
assert!(offset_of!(RawTransferMetadata2, chunk_ptr) == 8);
2122
};
2223

2324
#[cfg(target_pointer_width = "32")]
2425
const _: () = {
2526
// Padding: 2 bytes
2627
assert!(size_of::<RawTransferMetadata2>() == 24);
27-
assert!(align_of::<RawTransferMetadata2>() == 8);
28-
assert!(offset_of!(RawTransferMetadata2, data_offset) == 12);
28+
assert!(align_of::<RawTransferMetadata2>() == 4);
29+
assert!(offset_of!(RawTransferMetadata2, data_offset) == 8);
2930
assert!(offset_of!(RawTransferMetadata2, is_ts) == 20);
31+
assert!(offset_of!(RawTransferMetadata2, source_len) == 12);
3032
assert!(offset_of!(RawTransferMetadata2, id) == 16);
3133
assert!(offset_of!(RawTransferMetadata2, can_be_freed) == 21);
32-
assert!(offset_of!(RawTransferMetadata2, alloc_ptr) == 8);
33-
assert!(offset_of!(RawTransferMetadata2, _padding) == 0);
34+
assert!(offset_of!(RawTransferMetadata2, alloc_ptr) == 0);
35+
assert!(offset_of!(RawTransferMetadata2, chunk_ptr) == 4);
3436
};
3537

3638
#[cfg(not(any(target_pointer_width = "64", target_pointer_width = "32")))]

crates/oxc_allocator/src/generated/fixed_size_constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// To edit this generated file you have to edit `tasks/ast_tools/src/generators/raw_transfer.rs`.
33

44
#![expect(clippy::unreadable_literal)]
5+
#![allow(dead_code)]
56

67
pub const BUFFER_SIZE: usize = 2147483632;
78
pub const BUFFER_ALIGN: usize = 4294967296;

crates/oxc_allocator/src/lib.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,25 @@ mod pool_fixed_size;
8080
target_endian = "little"
8181
))]
8282
use pool_fixed_size as pool;
83-
// Import so that `generated/assert_layouts.rs` can access it
83+
// Import under original name so that `generated/assert_layouts.rs` can access it
8484
#[cfg(all(
8585
feature = "fixed_size",
8686
not(feature = "disable_fixed_size"),
8787
target_pointer_width = "64",
8888
target_endian = "little"
8989
))]
9090
use pool_fixed_size::RawTransferMetadata2;
91+
// Export so can be used in `napi/oxlint2`
92+
#[cfg(all(
93+
feature = "fixed_size",
94+
not(feature = "disable_fixed_size"),
95+
target_pointer_width = "64",
96+
target_endian = "little"
97+
))]
98+
#[doc(hidden)]
99+
pub use pool_fixed_size::{
100+
ALLOC_LAYOUT as FIXED_SIZE_ALLOC_LAYOUT, RawTransferMetadata2 as RawTransferMetadata,
101+
};
91102

92103
pub use pool::{AllocatorGuard, AllocatorPool};
93104

crates/oxc_allocator/src/pool_fixed_size.rs

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,10 @@ pub struct RawTransferMetadata2 {
113113
pub data_offset: u32,
114114
/// `true` if AST is TypeScript.
115115
pub is_ts: bool,
116+
/// Length of source text in UTF-8 bytes.
117+
pub source_len: u32,
116118
/// Unique ID of this allocator.
117-
pub(crate) id: u32,
119+
pub id: u32,
118120
/// `true` if memory allocation backing the `FixedSizeAllocator` can be freed.
119121
/// Is `true` if allocator is currently owned by only Rust or JS, but not both.
120122
///
@@ -124,25 +126,26 @@ pub struct RawTransferMetadata2 {
124126
/// Memory will be freed when the `FixedSizeAllocator` is dropped on Rust side.
125127
/// * Also be set to `true` if `FixedSizeAllocator` is dropped on Rust side.
126128
/// Memory will be freed in finalizer when JS garbage collector collects the buffer.
127-
pub(crate) can_be_freed: AtomicBool,
129+
pub can_be_freed: AtomicBool,
128130
/// Pointer to start of original allocation backing the `Allocator`.
129-
pub(crate) alloc_ptr: NonNull<u8>,
130-
/// Padding to pad struct to size 32.
131-
pub(crate) _padding: u64,
131+
pub alloc_ptr: NonNull<u8>,
132+
/// Pointer to start of `Allocator` chunk.
133+
pub chunk_ptr: NonNull<u8>,
132134
}
133135
use RawTransferMetadata2 as RawTransferMetadata;
134136

135137
const METADATA_SIZE: usize = size_of::<RawTransferMetadata>();
136138

137139
impl RawTransferMetadata {
138-
fn new(id: u32, alloc_ptr: NonNull<u8>) -> Self {
140+
fn new(id: u32, alloc_ptr: NonNull<u8>, chunk_ptr: NonNull<u8>) -> Self {
139141
Self {
140142
data_offset: 0,
141143
is_ts: false,
144+
source_len: 0,
142145
id,
143146
can_be_freed: AtomicBool::new(true),
144147
alloc_ptr,
145-
_padding: 0,
148+
chunk_ptr,
146149
}
147150
}
148151
}
@@ -165,7 +168,8 @@ impl RawTransferMetadata {
165168
const ALLOC_SIZE: usize = BUFFER_SIZE + TWO_GIB;
166169
const ALLOC_ALIGN: usize = TWO_GIB;
167170

168-
const ALLOC_LAYOUT: Layout = match Layout::from_size_align(ALLOC_SIZE, ALLOC_ALIGN) {
171+
/// Layout of backing allocations for fixed-size allocators.
172+
pub const ALLOC_LAYOUT: Layout = match Layout::from_size_align(ALLOC_SIZE, ALLOC_ALIGN) {
169173
Ok(layout) => layout,
170174
Err(_) => unreachable!(),
171175
};
@@ -222,7 +226,7 @@ impl FixedSizeAllocator {
222226
let allocator = unsafe { Allocator::from_raw_parts(chunk_ptr, CHUNK_SIZE) };
223227

224228
// Store metadata after allocator chunk
225-
let metadata = RawTransferMetadata::new(id, alloc_ptr);
229+
let metadata = RawTransferMetadata::new(id, alloc_ptr, chunk_ptr);
226230
// SAFETY: `chunk_ptr` is at least `BUFFER_SIZE` bytes from the end of the allocation.
227231
// `CHUNK_SIZE` had the size of `RawTransferMetadata` subtracted from it.
228232
// So there is space within the allocation for `RawTransferMetadata` after the `Allocator`'s chunk.
@@ -251,13 +255,28 @@ impl FixedSizeAllocator {
251255
}
252256

253257
impl Drop for FixedSizeAllocator {
258+
// TODO: Delete logging
254259
fn drop(&mut self) {
255260
// Get pointer to start of allocation backing this `FixedSizeAllocator`
256261
let alloc_ptr = {
257-
let metadata_ptr = self.allocator.end_ptr().cast::<RawTransferMetadata>();
258262
// SAFETY: `FixedSizeAllocator` is being dropped, so no other references to data in it may exist.
259263
// `FixedSizeAllocator::new` wrote `RawTransferMetadata` to the location pointed to by `end_ptr`.
260-
let metadata = unsafe { metadata_ptr.as_ref() };
264+
let metadata = unsafe { self.allocator.metadata() };
265+
266+
// If `can_be_freed` is already `true`, either this `Allocator` was never sent to JS side,
267+
// or JS garbage collector already collected it. We can deallocate the memory.
268+
// If not, set it to `true` and exit. Memory will be freed when JS garbage collector
269+
// collects the buffer.
270+
//
271+
// Maybe a more relaxed `Ordering` would be OK, but I (@overlookmotel) am not sure,
272+
// so going with `Ordering::SeqCst` to be on safe side.
273+
// Deallocation only happens at the end of the whole process, so it shouldn't matter much.
274+
// TODO: Figure out if can use `Ordering::Relaxed`.
275+
let can_be_freed = metadata.can_be_freed.fetch_or(true, Ordering::SeqCst);
276+
if !can_be_freed {
277+
return;
278+
}
279+
261280
metadata.alloc_ptr
262281
};
263282

@@ -269,3 +288,42 @@ impl Drop for FixedSizeAllocator {
269288
// SAFETY: `Allocator` is `Send`.
270289
// Moving `alloc_ptr: NonNull<u8>` across threads along with the `Allocator` is safe.
271290
unsafe impl Send for FixedSizeAllocator {}
291+
292+
impl Allocator {
293+
/// Get reference to [`RawTransferMetadata`] for this `Allocator`.
294+
///
295+
/// # SAFETY
296+
///
297+
/// * This [`Allocator`] must have been created by a fixed-size [`AllocatorPool`].
298+
/// * No mutable references to [`RawTransferMetadata`] for this `Allocator` can exist
299+
/// while the reference returned by this method lives.
300+
pub unsafe fn metadata(&self) -> &RawTransferMetadata {
301+
// SAFETY:
302+
// * Caller guarantees that this `Allocator` was created by a fixed-size `AllocatorPool`.
303+
// `FixedSizeAllocator::new` wrote `RawTransferMetadata` to the location pointed to by `end_ptr`.
304+
// * Caller guarantees no mutable references to `RawTransferMetadata` exist
305+
// while this reference lives.
306+
unsafe { self.end_ptr().cast::<RawTransferMetadata>().as_ref() }
307+
}
308+
309+
/// Get mutable reference to [`RawTransferMetadata`] for this `Allocator`.
310+
///
311+
/// # SAFETY
312+
///
313+
/// * This [`Allocator`] must have been created by a fixed-size [`AllocatorPool`].
314+
/// * No other references to [`RawTransferMetadata`] for this `Allocator` can exist
315+
/// while the reference returned by this method lives.
316+
///
317+
/// Note: This means we must be sure that buffer cannot be garbage-collected on JS side,
318+
/// as that creates a `&` ref to the `RawTransferMetadata`, and the finalizer that runs
319+
/// when that happens can run on another thread.
320+
#[expect(clippy::mut_from_ref)]
321+
pub unsafe fn metadata_mut(&self) -> &mut RawTransferMetadata {
322+
// SAFETY:
323+
// * Caller guarantees that this `Allocator` was created by a fixed-size `AllocatorPool`.
324+
// `FixedSizeAllocator::new` wrote `RawTransferMetadata` to the location pointed to by `end_ptr`.
325+
// * Caller guarantees no other references to `RawTransferMetadata` exist
326+
// while this reference lives.
327+
unsafe { self.end_ptr().cast::<RawTransferMetadata>().as_mut() }
328+
}
329+
}

crates/oxc_ast_macros/src/generated/structs.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ pub static STRUCTS: phf::Map<&'static str, StructDetails> = ::phf::Map {
135135
("Decorator", StructDetails { field_order: None }),
136136
("CharacterClass", StructDetails { field_order: Some(&[0, 2, 3, 4, 1]) }),
137137
("TemplateElementValue", StructDetails { field_order: None }),
138-
("RawTransferMetadata", StructDetails { field_order: Some(&[2, 4, 3, 5, 1, 0]) }),
138+
("RawTransferMetadata", StructDetails { field_order: Some(&[2, 5, 3, 4, 6, 0, 1]) }),
139139
("TSTypeParameter", StructDetails { field_order: None }),
140140
("SourceType", StructDetails { field_order: None }),
141141
("ErrorLabel", StructDetails { field_order: Some(&[1, 0]) }),
@@ -266,7 +266,7 @@ pub static STRUCTS: phf::Map<&'static str, StructDetails> = ::phf::Map {
266266
("ImportAttribute", StructDetails { field_order: None }),
267267
("TSConditionalType", StructDetails { field_order: None }),
268268
("TSNamespaceExportDeclaration", StructDetails { field_order: None }),
269-
("RawTransferMetadata2", StructDetails { field_order: Some(&[2, 4, 3, 5, 1, 0]) }),
269+
("RawTransferMetadata2", StructDetails { field_order: Some(&[2, 5, 3, 4, 6, 0, 1]) }),
270270
("AssignmentTargetWithDefault", StructDetails { field_order: None }),
271271
("RegExpLiteral", StructDetails { field_order: None }),
272272
("CapturingGroup", StructDetails { field_order: None }),

crates/oxc_linter/src/external_linter.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::{fmt::Debug, pin::Pin, sync::Arc};
22

33
use serde::{Deserialize, Serialize};
44

5+
use oxc_allocator::Allocator;
6+
57
pub type ExternalLinterLoadPluginCb = Arc<
68
dyn Fn(
79
String,
@@ -17,7 +19,11 @@ pub type ExternalLinterLoadPluginCb = Arc<
1719
>;
1820

1921
pub type ExternalLinterCb = Arc<
20-
dyn Fn(String, Vec<u32>) -> Result<Vec<LintResult>, Box<dyn std::error::Error + Send + Sync>>
22+
dyn Fn(
23+
String,
24+
Vec<u32>,
25+
&Allocator,
26+
) -> Result<Vec<LintResult>, Box<dyn std::error::Error + Send + Sync>>
2127
+ Sync
2228
+ Send,
2329
>;

crates/oxc_linter/src/lib.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![expect(clippy::self_named_module_files)] // for rules.rs
22
#![allow(clippy::literal_string_with_formatting_args)]
33

4-
use std::{path::Path, rc::Rc, sync::Arc};
4+
use std::{path::Path, ptr, rc::Rc, sync::Arc};
55

66
use oxc_allocator::Allocator;
77
use oxc_diagnostics::OxcDiagnostic;
@@ -118,7 +118,7 @@ impl Linter {
118118
path: &Path,
119119
semantic: Rc<Semantic<'a>>,
120120
module_record: Arc<ModuleRecord>,
121-
_allocator: &Allocator,
121+
allocator: &Allocator,
122122
) -> Vec<Message<'a>> {
123123
let ResolvedLinterState { rules, config, external_rules } = self.config.resolve(path);
124124

@@ -206,10 +206,29 @@ impl Linter {
206206

207207
if !external_rules.is_empty() {
208208
if let Some(external_linter) = self.external_linter.as_ref() {
209+
// Write offset of `Program` and source text length in metadata at end of buffer
210+
#[expect(clippy::missing_panics_doc, reason = "program() always returns `Some`")]
211+
let program = semantic.nodes().program().unwrap();
212+
let program_offset = ptr::from_ref(program) as u32;
213+
#[expect(clippy::cast_possible_truncation)]
214+
let source_len = program.source_text.len() as u32;
215+
{
216+
// SAFETY: TODO
217+
// Is this safe? If buffer is GC-ed on JS side, it would create a `&RawTransferMetadata`
218+
// which could alias this `&mut` ref. That shouldn't be possible as all buffers
219+
// are stored in an array on JS side, so they don't get GC-ed until process exits.
220+
// But this feels a bit danngerous.
221+
let metadata = unsafe { allocator.metadata_mut() };
222+
metadata.data_offset = program_offset;
223+
metadata.source_len = source_len;
224+
}
225+
226+
// Pass AST and rule IDs to JS
209227
let result = (external_linter.run)(
210228
#[expect(clippy::missing_panics_doc)]
211229
path.to_str().unwrap().to_string(),
212230
external_rules.iter().map(|(rule_id, _)| rule_id.raw()).collect(),
231+
allocator,
213232
);
214233
match result {
215234
Ok(diagnostics) => {

napi/oxlint2/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ test = false
2222
doctest = false
2323

2424
[dependencies]
25+
oxc_allocator = { workspace = true, features = ["fixed_size"] }
2526
oxlint = { workspace = true, features = ["oxlint2", "allocator"] }
2627

2728
napi = { workspace = true, features = ["async"] }

0 commit comments

Comments
 (0)