Skip to content

Commit 1f0f17f

Browse files
authored
Merge pull request #6281 from cloudflare/yagiz/add-feature-flags-rust
add FeatureFlags support to jsg rust
2 parents d604613 + 2164075 commit 1f0f17f

File tree

8 files changed

+206
-11
lines changed

8 files changed

+206
-11
lines changed

src/rust/AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
| Crate | Purpose |
1010
| -------------------- | ------------------------------------------------------------------------------------------------------ |
11-
| `jsg/` | Rust JSG bindings: `Lock`, `Ref<T>`, `Resource`, `Struct`, `Type`, `Realm`, module registration |
11+
| `jsg/` | Rust JSG bindings: `Lock`, `Ref<T>`, `Resource`, `Struct`, `Type`, `Realm`, `FeatureFlags`, module registration |
1212
| `jsg-macros/` | Proc macros: `#[jsg_struct]`, `#[jsg_method]`, `#[jsg_resource]`, `#[jsg_oneof]` |
1313
| `jsg-test/` | Test harness (`Harness`) for JSG Rust bindings |
1414
| `api/` | Rust-implemented Node.js APIs; registers modules via `register_nodejs_modules()` |
@@ -30,3 +30,4 @@
3030
- **Linting**: `just clippy <crate>` — pedantic+nursery; `allow-unwrap-in-tests`
3131
- **Tests**: inline `#[cfg(test)]` modules; JSG tests use `jsg_test::Harness::run_in_context()`
3232
- **FFI pointers**: functions receiving raw pointers must be `unsafe fn` (see `jsg/README.md`)
33+
- **Feature flags**: `Lock::feature_flags()` returns a capnp `compatibility_flags::Reader` for the current worker. Use `lock.feature_flags().get_node_js_compat()`. Flags are parsed once and stored in the `Realm` at construction; C++ passes canonical capnp bytes to `realm_create()`. Schema: `src/workerd/io/compatibility-date.capnp`, generated Rust bindings: `compatibility_date_capnp` crate.

src/rust/jsg-test/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ wd_cc_library(
1818
srcs = ["ffi.c++"],
1919
implementation_deps = [
2020
":lib.rs@cxx",
21+
"//src/workerd/io:compatibility-date_capnp",
22+
"@capnp-cpp//src/capnp:capnp",
2123
],
2224
visibility = ["//visibility:public"],
2325
deps = [

src/rust/jsg-test/ffi.c++

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "ffi.h"
22

3+
#include <workerd/io/compatibility-date.capnp.h>
34
#include <workerd/jsg/setup.h>
45
#include <workerd/rust/jsg-test/lib.rs.h>
56
#include <workerd/rust/jsg/ffi-inl.h>
@@ -9,6 +10,7 @@
910

1011
#include <v8.h>
1112

13+
#include <capnp/message.h>
1214
#include <kj/common.h>
1315

1416
using namespace kj_rs;
@@ -38,7 +40,14 @@ TestHarness::TestHarness(::workerd::jsg::V8StackScope&)
3840
: isolate(kj::heap<TestIsolate>(getV8System(), kj::heap<::workerd::jsg::IsolateObserver>())),
3941
locker(isolate->getIsolate()),
4042
isolateScope(isolate->getIsolate()),
41-
realm(::workerd::rust::jsg::realm_create(isolate->getIsolate())) {
43+
realm([&] {
44+
// Build default (all-false) feature flags for the test realm.
45+
capnp::MallocMessageBuilder flagsMessage;
46+
flagsMessage.initRoot<CompatibilityFlags>();
47+
auto words = capnp::canonicalize(flagsMessage.getRoot<CompatibilityFlags>().asReader());
48+
return ::workerd::rust::jsg::realm_create(
49+
isolate->getIsolate(), words.asBytes().as<Rust>());
50+
}()) {
4251
isolate->getIsolate()->SetData(::workerd::jsg::SetDataIndex::SET_DATA_RUST_REALM, &*realm);
4352
}
4453

src/rust/jsg/BUILD.bazel

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ wd_rust_crate(
1212
],
1313
cxx_bridge_tags = ["no-clang-tidy"],
1414
visibility = ["//visibility:public"],
15-
deps = [":ffi"],
15+
deps = [
16+
":ffi",
17+
"//src/workerd/io:compatibility-date_capnp_rust",
18+
"@crates_vendor//:capnp",
19+
],
1620
)
1721

1822
wd_cc_library(

src/rust/jsg/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ Rust bindings for the JSG (JavaScript Glue) layer, enabling Rust code to integra
77
Functions exposed to C++ via FFI that receive raw pointers must be marked as `unsafe fn`. The `unsafe` keyword indicates to callers that the function deals with raw pointers and requires careful handling.
88

99
```rust
10-
pub unsafe fn realm_create(isolate: *mut v8::ffi::Isolate) -> Box<Realm> {
10+
pub unsafe fn realm_create(
11+
isolate: *mut v8::ffi::Isolate,
12+
feature_flags_data: &[u8],
13+
) -> Box<Realm> {
1114
// implementation
1215
}
1316
```
@@ -38,3 +41,33 @@ pub fn process(&self, value: StringOrNumber) -> Result<String, jsg::Error> {
3841
```
3942

4043
This is similar to `kj::OneOf<>` in C++ JSG.
44+
45+
## Feature Flags (Compatibility Flags)
46+
47+
`Lock::feature_flags()` provides Rust-native access to the worker's compatibility flags, backed by the Cap'n Proto Rust crate (`capnp`). The flags are deserialized from the `CompatibilityFlags` schema in `src/workerd/io/compatibility-date.capnp`.
48+
49+
### Reading flags
50+
51+
```rust
52+
if lock.feature_flags().get_node_js_compat() {
53+
// Node.js compatibility behavior
54+
}
55+
```
56+
57+
`feature_flags()` returns a capnp-generated `compatibility_flags::Reader` with a getter for each boolean flag (e.g., `get_node_js_compat()`, `get_url_standard()`, `get_fetch_refuses_unknown_protocols()`).
58+
59+
### How it works
60+
61+
1. During worker initialization, C++ canonicalizes the worker's `CompatibilityFlags` via `capnp::canonicalize()` and passes the bytes to `realm_create()`, which parses them once and stores the result in the per-context `Realm`.
62+
2. `lock.feature_flags()` reads the cached `FeatureFlags` and returns its capnp reader. No copies or re-parsing on access.
63+
64+
### Key types and files
65+
66+
| Item | Location |
67+
|------|----------|
68+
| `FeatureFlags` struct | `src/rust/jsg/feature_flags.rs` |
69+
| `Lock::feature_flags()` | `src/rust/jsg/lib.rs` |
70+
| `realm_create()` FFI | `src/rust/jsg/lib.rs` (CXX bridge) |
71+
| C++ call site | `src/workerd/io/worker.c++` (`initIsolate`) |
72+
| Cap'n Proto schema | `src/workerd/io/compatibility-date.capnp` |
73+
| Generated Rust bindings | `//src/workerd/io:compatibility-date_capnp_rust` (Bazel target) |

src/rust/jsg/feature_flags.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//! Rust-native access to workerd compatibility flags.
2+
//!
3+
//! ```ignore
4+
//! if lock.feature_flags().get_node_js_compat() {
5+
//! // Node.js compatibility behavior
6+
//! }
7+
//! ```
8+
9+
use capnp::message::ReaderOptions;
10+
pub use compatibility_date_capnp::compatibility_flags;
11+
12+
/// Provides access to the current worker's compatibility flags.
13+
///
14+
/// Parsed once from canonical Cap'n Proto bytes during Realm construction
15+
/// and stored in the per-context [`Realm`](crate::Realm). Access via
16+
/// [`Lock::feature_flags()`](crate::Lock::feature_flags).
17+
pub struct FeatureFlags {
18+
message: capnp::message::Reader<Vec<Vec<u8>>>,
19+
}
20+
21+
impl FeatureFlags {
22+
/// Create from canonical (single-segment, no segment table) Cap'n Proto bytes.
23+
///
24+
/// On the C++ side, produce these via `capnp::canonicalize(reader)`.
25+
///
26+
/// # Panics
27+
///
28+
/// Panics if `data` is empty or not word-aligned.
29+
pub(crate) fn from_bytes(data: &[u8]) -> Self {
30+
assert!(!data.is_empty(), "FeatureFlags data must not be empty");
31+
assert!(
32+
data.len().is_multiple_of(8),
33+
"FeatureFlags data must be word-aligned (got {} bytes)",
34+
data.len()
35+
);
36+
let segments = vec![data.to_vec()];
37+
let message = capnp::message::Reader::new(segments, ReaderOptions::new());
38+
Self { message }
39+
}
40+
41+
/// Returns the `CompatibilityFlags` reader.
42+
///
43+
/// The reader has a getter for each flag defined in `compatibility-date.capnp`
44+
/// (e.g., `get_node_js_compat()`).
45+
///
46+
/// # Panics
47+
///
48+
/// Panics if the stored message has an invalid capnp root (should never happen
49+
/// when constructed via `from_bytes`).
50+
pub fn reader(&self) -> compatibility_flags::Reader<'_> {
51+
self.message
52+
.get_root::<compatibility_flags::Reader<'_>>()
53+
.expect("Invalid FeatureFlags capnp root")
54+
}
55+
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use super::*;
60+
61+
/// Helper: build a `CompatibilityFlags` capnp message with the given flag setter,
62+
/// return the raw single-segment bytes (no wire-format header).
63+
fn build_flags<F>(setter: F) -> Vec<u8>
64+
where
65+
F: FnOnce(compatibility_flags::Builder<'_>),
66+
{
67+
let mut message = capnp::message::Builder::new_default();
68+
{
69+
let flags = message.init_root::<compatibility_flags::Builder<'_>>();
70+
setter(flags);
71+
}
72+
let output = message.get_segments_for_output();
73+
output[0].to_vec()
74+
}
75+
76+
#[test]
77+
fn from_bytes_roundtrip() {
78+
let bytes = build_flags(|mut f| {
79+
f.set_node_js_compat(true);
80+
});
81+
let ff = FeatureFlags::from_bytes(&bytes);
82+
assert!(ff.reader().get_node_js_compat());
83+
}
84+
85+
#[test]
86+
#[should_panic(expected = "FeatureFlags data must not be empty")]
87+
fn from_bytes_empty_panics() {
88+
FeatureFlags::from_bytes(&[]);
89+
}
90+
91+
#[test]
92+
fn default_flags_are_false() {
93+
let bytes = build_flags(|_| {});
94+
let ff = FeatureFlags::from_bytes(&bytes);
95+
assert!(!ff.reader().get_node_js_compat());
96+
assert!(!ff.reader().get_node_js_compat_v2());
97+
assert!(!ff.reader().get_fetch_refuses_unknown_protocols());
98+
}
99+
100+
#[test]
101+
fn multiple_flags() {
102+
let bytes = build_flags(|mut f| {
103+
f.set_node_js_compat(true);
104+
f.set_node_js_compat_v2(true);
105+
f.set_fetch_refuses_unknown_protocols(false);
106+
});
107+
let ff = FeatureFlags::from_bytes(&bytes);
108+
assert!(ff.reader().get_node_js_compat());
109+
assert!(ff.reader().get_node_js_compat_v2());
110+
assert!(!ff.reader().get_fetch_refuses_unknown_protocols());
111+
}
112+
113+
#[test]
114+
fn reader_called_multiple_times() {
115+
let bytes = build_flags(|mut f| {
116+
f.set_node_js_compat(true);
117+
});
118+
let ff = FeatureFlags::from_bytes(&bytes);
119+
// Reader can be obtained multiple times from the same FeatureFlags.
120+
assert!(ff.reader().get_node_js_compat());
121+
assert!(ff.reader().get_node_js_compat());
122+
}
123+
}

src/rust/jsg/lib.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ use std::rc::Rc;
99

1010
use kj_rs::KjMaybe;
1111

12+
pub mod feature_flags;
1213
pub mod modules;
1314
pub mod v8;
1415
mod wrappable;
1516

17+
pub use feature_flags::FeatureFlags;
1618
pub use v8::BigInt64Array;
1719
pub use v8::BigUint64Array;
1820
pub use v8::Float32Array;
@@ -32,8 +34,11 @@ mod ffi {
3234
extern "Rust" {
3335
type Realm;
3436

37+
/// Create a fully-initialized Realm with feature flags.
38+
/// `feature_flags_data` is canonical (single-segment, no segment table) Cap'n Proto
39+
/// bytes produced by `capnp::canonicalize()` on the C++ side.
3540
#[expect(clippy::unnecessary_box_returns)]
36-
unsafe fn realm_create(isolate: *mut Isolate) -> Box<Realm>;
41+
unsafe fn realm_create(isolate: *mut Isolate, feature_flags_data: &[u8]) -> Box<Realm>;
3742
}
3843

3944
unsafe extern "C++" {
@@ -630,10 +635,21 @@ impl Lock {
630635
todo!()
631636
}
632637

633-
fn realm(&mut self) -> &mut Realm {
638+
pub(crate) fn realm(&mut self) -> &mut Realm {
634639
unsafe { &mut *crate::ffi::realm_from_isolate(self.isolate().as_ffi()) }
635640
}
636641

642+
/// Returns the current worker's compatibility flags reader.
643+
///
644+
/// ```ignore
645+
/// if lock.feature_flags().get_node_js_compat() {
646+
/// // Node.js compatibility behavior
647+
/// }
648+
/// ```
649+
pub fn feature_flags(&mut self) -> compatibility_date_capnp::compatibility_flags::Reader<'_> {
650+
self.realm().feature_flags.reader()
651+
}
652+
637653
/// Throws an error as a V8 exception.
638654
pub fn throw_exception(&mut self, err: &Error) {
639655
unsafe {
@@ -876,14 +892,17 @@ pub unsafe fn drop_resource<R: Resource>(_isolate: *mut ffi::Isolate, this: *mut
876892
pub struct Realm {
877893
isolate: v8::IsolatePtr,
878894
resources: Vec<*mut ResourceState>,
895+
/// Parsed `CompatibilityFlags` capnp message, initialized at construction.
896+
feature_flags: FeatureFlags,
879897
}
880898

881899
impl Realm {
882-
/// Creates a new Realm from a V8 isolate.
883-
pub fn from_isolate(isolate: v8::IsolatePtr) -> Self {
900+
/// Creates a new Realm with its feature flags.
901+
pub fn new(isolate: v8::IsolatePtr, feature_flags: FeatureFlags) -> Self {
884902
Self {
885903
isolate,
886904
resources: Vec::new(),
905+
feature_flags,
887906
}
888907
}
889908

@@ -928,6 +947,7 @@ impl Drop for Realm {
928947
}
929948

930949
#[expect(clippy::unnecessary_box_returns)]
931-
unsafe fn realm_create(isolate: *mut v8::ffi::Isolate) -> Box<Realm> {
932-
unsafe { Box::new(Realm::from_isolate(v8::IsolatePtr::from_ffi(isolate))) }
950+
unsafe fn realm_create(isolate: *mut v8::ffi::Isolate, feature_flags_data: &[u8]) -> Box<Realm> {
951+
let feature_flags = FeatureFlags::from_bytes(feature_flags_data);
952+
unsafe { Box::new(Realm::new(v8::IsolatePtr::from_ffi(isolate), feature_flags)) }
933953
}

src/workerd/io/worker.c++

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,9 +732,12 @@ struct Worker::Isolate::Impl {
732732
kj::Maybe<std::unique_ptr<v8_inspector::V8Inspector>> inspector;
733733
jsg::runInV8Stack([&](jsg::V8StackScope& stackScope) {
734734
auto lock = api.lock(stackScope);
735-
realm = ::workerd::rust::jsg::realm_create(lock->v8Isolate);
735+
auto featureFlagsWords = capnp::canonicalize(api.getFeatureFlags());
736+
realm = ::workerd::rust::jsg::realm_create(
737+
lock->v8Isolate, featureFlagsWords.asBytes().as<kj_rs::Rust>());
736738
lock->v8Isolate->SetData(
737739
::workerd::jsg::SetDataIndex::SET_DATA_RUST_REALM, &*KJ_REQUIRE_NONNULL(realm));
740+
738741
limitEnforcer.customizeIsolate(lock->v8Isolate);
739742
if (inspectorPolicy != InspectorPolicy::DISALLOW) {
740743
// We just created our isolate, so we don't need to use Isolate::Impl::Lock.

0 commit comments

Comments
 (0)