From 4f87f2c834c1ee47398b6ebc4c9077655a086f50 Mon Sep 17 00:00:00 2001 From: Roger Chen Date: Sat, 10 Oct 2026 12:00:00 -0400 Subject: [PATCH 01/11] Add FFI crate and context lifecycle --- Cargo.lock | 64 +++++++++++++++++++++++++++++++++++++----- Cargo.toml | 2 ++ ffi/boa_ffi/Cargo.toml | 21 ++++++++++++++ ffi/boa_ffi/src/lib.rs | 39 +++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 ffi/boa_ffi/Cargo.toml create mode 100644 ffi/boa_ffi/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b4483f70d11..8c2c5bce561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,6 +480,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "boa_ffi" +version = "0.22.0" +dependencies = [ + "boa_engine", + "diplomat", + "diplomat-runtime", +] + [[package]] name = "boa_gc" version = "0.22.0" @@ -923,7 +932,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1143,7 +1152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ "data-encoding", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] @@ -1202,6 +1211,38 @@ dependencies = [ "thousands", ] +[[package]] +name = "diplomat" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7935649d00000f5c5d735448ad3dc07b9738160727017914cf42138b8e8e6611" +dependencies = [ + "diplomat_core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "diplomat-runtime" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970ac38ad677632efcee6d517e783958da9bc78ec206d8d5e35b459ffc5e4864" + +[[package]] +name = "diplomat_core" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf41b94101a4bce993febaf0098092b0bb31deaf0ecaf6e0a2562465f61b383" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "smallvec", + "strck", + "syn 2.0.119", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -1310,7 +1351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3304,7 +3345,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3361,7 +3402,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3746,6 +3787,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strck" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f" +dependencies = [ + "unicode-ident", +] + [[package]] name = "string-interner" version = "0.19.0" @@ -3905,7 +3955,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4738,7 +4788,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index dcbf179db1a..12210edf17d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,8 @@ bitflags = "2.13.1" clap = "4.6.6" colored = "3.1.1" cow-utils = "0.1.3" +diplomat = "=0.15.0" +diplomat-runtime = "=0.15.1" fast-float2 = "0.2.4" hashbrown = "0.17.1" http = { version = "1.5.0" } diff --git a/ffi/boa_ffi/Cargo.toml b/ffi/boa_ffi/Cargo.toml new file mode 100644 index 00000000000..34d9603ee3e --- /dev/null +++ b/ffi/boa_ffi/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "boa_ffi" +description = "C bindings for the Boa JavaScript engine" +publish = false +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +boa_engine.workspace = true +diplomat.workspace = true +diplomat-runtime.workspace = true + +[lints] +workspace = true diff --git a/ffi/boa_ffi/src/lib.rs b/ffi/boa_ffi/src/lib.rs new file mode 100644 index 00000000000..0958fe1c409 --- /dev/null +++ b/ffi/boa_ffi/src/lib.rs @@ -0,0 +1,39 @@ +//! C bindings for the Boa JavaScript engine. +//! +//! Rust applications should use `boa_engine` directly. + +/// Entry points used by the generated C bindings. +#[diplomat::bridge] +pub mod ffi { + use boa_engine::{Context, context::ContextBuilder}; + + /// An owned Boa context, exposed as an opaque C handle. + /// + /// Keep the handle on the thread that created it. Destroy it exactly once + /// with `BoaContext_destroy`; copying the pointer does not copy ownership. + /// The destructor requires a live, non-null handle. Do not call `free` on it. + #[derive(Debug)] + #[diplomat::opaque_mut] + pub struct BoaContext { + _context: Context, + } + + impl BoaContext { + /// Creates a context, or returns null if engine initialization fails. + /// + /// A successful call transfers ownership to the caller. Multiple contexts + /// can coexist on one thread. Unexpected Rust panics and allocation + /// failures may abort the process rather than return null. + #[must_use] + pub fn new() -> Option> { + Self::from_builder(Context::builder()) + } + + fn from_builder(builder: ContextBuilder) -> Option> { + builder + .build() + .ok() + .map(|context| Box::new(Self { _context: context })) + } + } +} From 2e0d549bb05b7a11d9a02094f6bdc2ca86a0d7da Mon Sep 17 00:00:00 2001 From: Roger Chen Date: Mon, 12 Oct 2026 12:00:00 -0400 Subject: [PATCH 02/11] Add reproducible C header generation --- Cargo.lock | 143 +++++++++++++++++++++ Cargo.toml | 1 + ffi/boa_ffi/c/generated/BoaContext.d.h | 19 +++ ffi/boa_ffi/c/generated/BoaContext.h | 26 ++++ ffi/boa_ffi/c/generated/diplomat_runtime.h | 82 ++++++++++++ tools/diplomat-gen/Cargo.toml | 16 +++ tools/diplomat-gen/src/main.rs | 28 ++++ 7 files changed, 315 insertions(+) create mode 100644 ffi/boa_ffi/c/generated/BoaContext.d.h create mode 100644 ffi/boa_ffi/c/generated/BoaContext.h create mode 100644 ffi/boa_ffi/c/generated/diplomat_runtime.h create mode 100644 tools/diplomat-gen/Cargo.toml create mode 100644 tools/diplomat-gen/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 8c2c5bce561..b5480b9bad7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,58 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "askama" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b8246bcbf8eb97abef10c2d92166449680d41d55c0fc6978a91dec2e3619608" +dependencies = [ + "askama_macros", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9670bc84a28bb3da91821ef74226949ab63f1265aff7c751634f1dd0e6f97c" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash 2.1.3", + "serde", + "serde_derive", + "syn 2.0.119", +] + +[[package]] +name = "askama_macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0756b45480437dded0565dfc568af62ccce146fb6cfe902e808ba86e445f44f" +dependencies = [ + "askama_derive", +] + +[[package]] +name = "askama_parser" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0af3691ba3af77949c0b5a3925444b85cb58a0184cc7fec16c68ba2e7be868" +dependencies = [ + "rustc-hash 2.1.3", + "serde", + "serde_derive", + "unicode-ident", + "winnow 1.0.4", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -299,6 +351,15 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.13.2" @@ -1223,18 +1284,49 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "diplomat-gen" +version = "0.22.0" +dependencies = [ + "diplomat-tool", +] + [[package]] name = "diplomat-runtime" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "970ac38ad677632efcee6d517e783958da9bc78ec206d8d5e35b459ffc5e4864" +[[package]] +name = "diplomat-tool" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a742eeda95cd614873b815c4577519b07b737e2dceaf65f47d983bbdaabc5d7" +dependencies = [ + "askama", + "clap", + "colored", + "diplomat_core", + "displaydoc", + "heck", + "indenter", + "itertools 0.14.0", + "pulldown-cmark", + "quote", + "serde", + "syn 2.0.119", + "syn-inline-mod", + "toml 0.8.23", +] + [[package]] name = "diplomat_core" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf41b94101a4bce993febaf0098092b0bb31deaf0ecaf6e0a2562465f61b383" dependencies = [ + "displaydoc", + "either", "proc-macro2", "quote", "serde", @@ -1630,6 +1722,15 @@ dependencies = [ "simple_logger", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -3118,6 +3219,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quote" version = "1.0.47" @@ -3870,6 +3990,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-inline-mod" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa6dca1fdb7b2ed46dd534a326725419d4fb10f23d8c85a8b2860e5eb25d0f9" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -4247,6 +4377,7 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", + "toml_write", "winnow 0.7.15", ] @@ -4271,6 +4402,12 @@ dependencies = [ "winnow 1.0.4", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.1.2+spec-1.1.0" @@ -4396,6 +4533,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 12210edf17d..10113004442 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ colored = "3.1.1" cow-utils = "0.1.3" diplomat = "=0.15.0" diplomat-runtime = "=0.15.1" +diplomat-tool = "=0.15.0" fast-float2 = "0.2.4" hashbrown = "0.17.1" http = { version = "1.5.0" } diff --git a/ffi/boa_ffi/c/generated/BoaContext.d.h b/ffi/boa_ffi/c/generated/BoaContext.d.h new file mode 100644 index 00000000000..da7027fa890 --- /dev/null +++ b/ffi/boa_ffi/c/generated/BoaContext.d.h @@ -0,0 +1,19 @@ +#ifndef BoaContext_D_H +#define BoaContext_D_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + + + + +typedef struct BoaContext BoaContext; + + + + +#endif // BoaContext_D_H diff --git a/ffi/boa_ffi/c/generated/BoaContext.h b/ffi/boa_ffi/c/generated/BoaContext.h new file mode 100644 index 00000000000..a2f582bc66c --- /dev/null +++ b/ffi/boa_ffi/c/generated/BoaContext.h @@ -0,0 +1,26 @@ +#ifndef BoaContext_H +#define BoaContext_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + +#include "BoaContext.d.h" + + + + + + +BoaContext* BoaContext_new(void); + +void BoaContext_destroy(BoaContext* self); + + + + + +#endif // BoaContext_H diff --git a/ffi/boa_ffi/c/generated/diplomat_runtime.h b/ffi/boa_ffi/c/generated/diplomat_runtime.h new file mode 100644 index 00000000000..e5d6b298d73 --- /dev/null +++ b/ffi/boa_ffi/c/generated/diplomat_runtime.h @@ -0,0 +1,82 @@ +#ifndef DIPLOMAT_RUNTIME_C_H +#define DIPLOMAT_RUNTIME_C_H + +#include +#include +#include +#include + +// These come from `uchar.h`, which is not available on all platforms. +// Redefining them in C is no problem, however in >C++11 they are fundamental +// types, which don't like being redefined. +#if !(__cplusplus >= 201100) +// https://en.cppreference.com/w/c/string/multibyte/char16_t +typedef uint_least16_t char16_t; +// https://en.cppreference.com/w/c/string/multibyte/char32_t +typedef uint_least32_t char32_t; +#endif + +static_assert(sizeof(char) == sizeof(uint8_t), "your architecture's `char` is not 8 bits"); +static_assert(sizeof(char16_t) == sizeof(uint16_t), "your architecture's `char16_t` is not 16 bits"); +static_assert(sizeof(char32_t) == sizeof(uint32_t), "your architecture's `char32_t` is not 32 bits"); + +typedef struct DiplomatWrite { + void* context; + char* buf; + size_t len; + size_t cap; + bool grow_failed; + void (*flush)(struct DiplomatWrite*); + bool (*grow)(struct DiplomatWrite*, size_t); +} DiplomatWrite; + +bool diplomat_is_str(const char* buf, size_t len); + +#define MAKE_SLICES(name, c_ty) \ + typedef struct Diplomat##name##View { \ + const c_ty* data; \ + size_t len; \ + } Diplomat##name##View; \ + typedef struct Diplomat##name##ViewMut { \ + c_ty* data; \ + size_t len; \ + } Diplomat##name##ViewMut; \ + typedef struct Diplomat##name##Array { \ + const c_ty* data; \ + size_t len; \ + } Diplomat##name##Array; + +#define MAKE_SLICES_AND_OPTIONS(name, c_ty) \ + MAKE_SLICES(name, c_ty) \ + typedef struct Option##name {union { c_ty ok; }; bool is_ok; } Option##name; \ + typedef struct Option##name##View {union { Diplomat##name##View ok; }; bool is_ok; } Option##name##View; \ + typedef struct Option##name##ViewMut {union { Diplomat##name##ViewMut ok; }; bool is_ok; } Option##name##ViewMut; \ + typedef struct Option##name##Array {union { Diplomat##name##Array ok; }; bool is_ok; } Option##name##Array; \ + +MAKE_SLICES_AND_OPTIONS(I8, int8_t) +MAKE_SLICES_AND_OPTIONS(U8, uint8_t) +MAKE_SLICES_AND_OPTIONS(I16, int16_t) +MAKE_SLICES_AND_OPTIONS(U16, uint16_t) +MAKE_SLICES_AND_OPTIONS(I32, int32_t) +MAKE_SLICES_AND_OPTIONS(U32, uint32_t) +MAKE_SLICES_AND_OPTIONS(I64, int64_t) +MAKE_SLICES_AND_OPTIONS(U64, uint64_t) +MAKE_SLICES_AND_OPTIONS(Isize, intptr_t) +MAKE_SLICES_AND_OPTIONS(Usize, size_t) +MAKE_SLICES_AND_OPTIONS(F32, float) +MAKE_SLICES_AND_OPTIONS(F64, double) +MAKE_SLICES_AND_OPTIONS(Bool, bool) +MAKE_SLICES_AND_OPTIONS(Char, char32_t) +MAKE_SLICES_AND_OPTIONS(String, char) +MAKE_SLICES_AND_OPTIONS(String16, char16_t) +MAKE_SLICES_AND_OPTIONS(Strings, DiplomatStringView) +MAKE_SLICES_AND_OPTIONS(Strings16, DiplomatString16View) + +DiplomatWrite diplomat_simple_write(char* buf, size_t buf_size); + +DiplomatWrite* diplomat_buffer_write_create(size_t cap); +char* diplomat_buffer_write_get_bytes(DiplomatWrite* t); +size_t diplomat_buffer_write_len(DiplomatWrite* t); +void diplomat_buffer_write_destroy(DiplomatWrite* t); + +#endif diff --git a/tools/diplomat-gen/Cargo.toml b/tools/diplomat-gen/Cargo.toml new file mode 100644 index 00000000000..bd9e2fafd27 --- /dev/null +++ b/tools/diplomat-gen/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "diplomat-gen" +description = "Generate Boa's C bindings with Diplomat" +publish = false +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +diplomat-tool.workspace = true + +[lints] +workspace = true diff --git a/tools/diplomat-gen/src/main.rs b/tools/diplomat-gen/src/main.rs new file mode 100644 index 00000000000..a83241b5a8e --- /dev/null +++ b/tools/diplomat-gen/src/main.rs @@ -0,0 +1,28 @@ +//! Regenerates the checked-in C headers for `boa_ffi`. + +use std::{fs, io, path::Path}; + +use diplomat_tool::{DocsUrlGenerator, config::Config}; + +fn main() -> io::Result<()> { + let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../ffi/boa_ffi"); + let headers = crate_dir.join("c/generated"); + + // This directory contains only generated files. Remove obsolete headers too. + match fs::remove_dir_all(&headers) { + Ok(()) => {} + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(error) => return Err(error), + } + + let mut config = Config::default(); + config.shared_config.lib_name = Some("boa_ffi".into()); + diplomat_tool::r#gen( + &crate_dir.join("src/lib.rs"), + "c", + &headers, + &DocsUrlGenerator::default(), + config, + false, + ) +} From 90dbdc64085bc3e99b09628fa047a4b006da5fc8 Mon Sep 17 00:00:00 2001 From: Jason Milad Date: Wed, 14 Oct 2026 12:00:00 -0400 Subject: [PATCH 03/11] Add Rust context tests --- ffi/boa_ffi/src/lib.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/ffi/boa_ffi/src/lib.rs b/ffi/boa_ffi/src/lib.rs index 0958fe1c409..e13d1f6f082 100644 --- a/ffi/boa_ffi/src/lib.rs +++ b/ffi/boa_ffi/src/lib.rs @@ -36,4 +36,41 @@ pub mod ffi { .map(|context| Box::new(Self { _context: context })) } } + + #[cfg(test)] + mod tests { + use super::{BoaContext, Context}; + + #[test] + fn initialization_failure_returns_none() { + let context = BoaContext::new().expect("default context should initialize"); + + // A blocking context cannot share a thread with this live context. + assert!(BoaContext::from_builder(Context::builder().can_block(true)).is_none()); + + drop(context); + + // Destruction must release the engine's per-thread context state. + let blocking = BoaContext::from_builder(Context::builder().can_block(true)) + .expect("blocking context should initialize after the conflict is gone"); + drop(blocking); + } + + #[test] + fn contexts_can_coexist_and_be_destroyed_in_any_order() { + let first = BoaContext::new().expect("first context should initialize"); + let second = BoaContext::new().expect("second context should initialize"); + drop(first); + + let third = BoaContext::new().expect("third context should initialize"); + drop(second); + drop(third); + + let blocking = Context::builder() + .can_block(true) + .build() + .expect("all nonblocking contexts should have been released"); + drop(blocking); + } + } } From ae0a47e0327ede6393c69fa4172fb6a0d8143576 Mon Sep 17 00:00:00 2001 From: Roger Chen Date: Fri, 16 Oct 2026 12:00:00 -0400 Subject: [PATCH 04/11] Add C context tests and runner --- ffi/boa_ffi/c/tests/context.c | 36 +++++++++++++++++++++++++++++++++++ ffi/boa_ffi/c/tests/run.sh | 28 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 ffi/boa_ffi/c/tests/context.c create mode 100755 ffi/boa_ffi/c/tests/run.sh diff --git a/ffi/boa_ffi/c/tests/context.c b/ffi/boa_ffi/c/tests/context.c new file mode 100644 index 00000000000..44aae734693 --- /dev/null +++ b/ffi/boa_ffi/c/tests/context.c @@ -0,0 +1,36 @@ +#include +#include + +#include "BoaContext.h" + +int main(void) { + for (size_t i = 0; i < 32; ++i) { + BoaContext *first = BoaContext_new(); + if (first == NULL) { + fputs("first context failed to initialize\n", stderr); + return EXIT_FAILURE; + } + + BoaContext *second = BoaContext_new(); + if (second == NULL) { + BoaContext_destroy(first); + fputs("second context failed to initialize\n", stderr); + return EXIT_FAILURE; + } + + BoaContext_destroy(first); + + BoaContext *third = BoaContext_new(); + if (third == NULL) { + BoaContext_destroy(second); + fputs("third context failed to initialize\n", stderr); + return EXIT_FAILURE; + } + + BoaContext_destroy(second); + BoaContext_destroy(third); + } + + puts("C context lifecycle tests passed."); + return EXIT_SUCCESS; +} diff --git a/ffi/boa_ffi/c/tests/run.sh b/ffi/boa_ffi/c/tests/run.sh new file mode 100755 index 00000000000..cfac0a5ba0f --- /dev/null +++ b/ffi/boa_ffi/c/tests/run.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH= cd -- "$script_dir/../../../.." && pwd) +cd "$repo_root" + +case $(uname -s) in + Darwin) library_name=libboa_ffi.dylib ;; + Linux) library_name=libboa_ffi.so ;; + *) echo "The C test runner supports native macOS and Linux builds." >&2; exit 1 ;; +esac + +# Select the host explicitly so Cargo's configured target cannot cause us to +# accidentally link a stale native library after a cross-compilation build. +host=$(rustc -vV | sed -n 's/^host: //p') +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-"$repo_root/target"}" +cargo build --locked -p boa_ffi --target "$host" +library_dir=$(CDPATH= cd -- "$CARGO_TARGET_DIR/$host/debug" && pwd) + +test_dir=$(mktemp -d "${TMPDIR:-/tmp}/boa-ffi-c.XXXXXX") +trap 'rm -rf "$test_dir"' EXIT HUP INT TERM + +"${CC:-cc}" -std=c11 -Wall -Wextra -Werror -pedantic \ + -I "$script_dir/../generated" "$script_dir/context.c" \ + "$library_dir/$library_name" "-Wl,-rpath,$library_dir" \ + -o "$test_dir/context" +"$test_dir/context" From 11167f0ca85b84854a02cd7506abceae4ca3c0a3 Mon Sep 17 00:00:00 2001 From: Jason Milad Date: Mon, 19 Oct 2026 12:00:00 -0400 Subject: [PATCH 05/11] Add value handles and matching headers --- ffi/boa_ffi/c/generated/JsValue.d.h | 19 ++++++ ffi/boa_ffi/c/generated/JsValue.h | 36 +++++++++++ ffi/boa_ffi/c/generated/JsValueType.d.h | 29 +++++++++ ffi/boa_ffi/c/generated/JsValueType.h | 22 +++++++ ffi/boa_ffi/src/lib.rs | 84 ++++++++++++++++++++++++- 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 ffi/boa_ffi/c/generated/JsValue.d.h create mode 100644 ffi/boa_ffi/c/generated/JsValue.h create mode 100644 ffi/boa_ffi/c/generated/JsValueType.d.h create mode 100644 ffi/boa_ffi/c/generated/JsValueType.h diff --git a/ffi/boa_ffi/c/generated/JsValue.d.h b/ffi/boa_ffi/c/generated/JsValue.d.h new file mode 100644 index 00000000000..30d353ff7b4 --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsValue.d.h @@ -0,0 +1,19 @@ +#ifndef JsValue_D_H +#define JsValue_D_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + + + + +typedef struct JsValue JsValue; + + + + +#endif // JsValue_D_H diff --git a/ffi/boa_ffi/c/generated/JsValue.h b/ffi/boa_ffi/c/generated/JsValue.h new file mode 100644 index 00000000000..b341bf8fed0 --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsValue.h @@ -0,0 +1,36 @@ +#ifndef JsValue_H +#define JsValue_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + +#include "JsValueType.d.h" + +#include "JsValue.d.h" + + + + + + +JsValue* JsValue_from_number(double number); + +JsValue* JsValue_from_null(void); + +JsValue* JsValue_from_undefined(void); + +JsValueType JsValue_get_type(const JsValue* self); + +typedef struct JsValue_as_number_result {union {double ok; }; bool is_ok;} JsValue_as_number_result; +JsValue_as_number_result JsValue_as_number(const JsValue* self); + +void JsValue_destroy(JsValue* self); + + + + + +#endif // JsValue_H diff --git a/ffi/boa_ffi/c/generated/JsValueType.d.h b/ffi/boa_ffi/c/generated/JsValueType.d.h new file mode 100644 index 00000000000..6a2b37dbe7b --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsValueType.d.h @@ -0,0 +1,29 @@ +#ifndef JsValueType_D_H +#define JsValueType_D_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + + + + +typedef enum JsValueType { + JsValueType_Undefined = 0, + JsValueType_Null = 1, + JsValueType_Boolean = 2, + JsValueType_Number = 3, + JsValueType_String = 4, + JsValueType_Symbol = 5, + JsValueType_BigInt = 6, + JsValueType_Object = 7, +} JsValueType; + +typedef struct JsValueType_option {union { JsValueType ok; }; bool is_ok; } JsValueType_option; + + + +#endif // JsValueType_D_H diff --git a/ffi/boa_ffi/c/generated/JsValueType.h b/ffi/boa_ffi/c/generated/JsValueType.h new file mode 100644 index 00000000000..f78398854e1 --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsValueType.h @@ -0,0 +1,22 @@ +#ifndef JsValueType_H +#define JsValueType_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + +#include "JsValueType.d.h" + + + + +// No Content + + + + + +#endif // JsValueType_H diff --git a/ffi/boa_ffi/src/lib.rs b/ffi/boa_ffi/src/lib.rs index e13d1f6f082..bdefdc559f5 100644 --- a/ffi/boa_ffi/src/lib.rs +++ b/ffi/boa_ffi/src/lib.rs @@ -4,8 +4,13 @@ /// Entry points used by the generated C bindings. #[diplomat::bridge] +#[allow( + clippy::unnecessary_box_returns, + reason = "Diplomat transfers opaque handle ownership to C through boxed pointers" +)] pub mod ffi { - use boa_engine::{Context, context::ContextBuilder}; + use boa_engine::{Context, JsValue as BoaJsValue, context::ContextBuilder, value::Type}; + use diplomat_runtime::DiplomatOption; /// An owned Boa context, exposed as an opaque C handle. /// @@ -37,6 +42,83 @@ pub mod ffi { } } + /// The ECMAScript language type of a value. + /// + /// Unlike JavaScript's `typeof`, this distinguishes null from objects. + /// Functions have the object type. + #[derive(Debug, PartialEq, Eq)] + pub enum JsValueType { + /// The undefined type. + Undefined, + /// The null type. + Null, + /// The boolean type. + Boolean, + /// The number type. + Number, + /// The string type. + String, + /// The symbol type. + Symbol, + /// The bigint type. + BigInt, + /// The object type, including functions. + Object, + } + + /// An owned JavaScript value, exposed as an opaque C handle. + /// + /// Keep it on its creating thread and destroy it exactly once with + /// `JsValue_destroy`. Getters borrow a live, non-null handle without + /// transferring ownership. Do not call `free` on it. + #[derive(Debug)] + #[diplomat::opaque] + pub struct JsValue(BoaJsValue); + + impl JsValue { + /// Creates an owned number value without requiring a context. + #[must_use] + pub fn from_number(number: f64) -> Box { + Box::new(Self(BoaJsValue::from(number))) + } + + /// Creates an owned null value without requiring a context. + #[must_use] + pub fn from_null() -> Box { + Box::new(Self(BoaJsValue::null())) + } + + /// Creates an owned undefined value without requiring a context. + #[must_use] + pub fn from_undefined() -> Box { + Box::new(Self(BoaJsValue::undefined())) + } + + /// Returns the language type without conversion or JavaScript execution. + #[must_use] + pub fn get_type(&self) -> JsValueType { + match self.0.get_type() { + Type::Undefined => JsValueType::Undefined, + Type::Null => JsValueType::Null, + Type::Boolean => JsValueType::Boolean, + Type::Number => JsValueType::Number, + Type::String => JsValueType::String, + Type::Symbol => JsValueType::Symbol, + Type::BigInt => JsValueType::BigInt, + Type::Object => JsValueType::Object, + } + } + + /// Returns the number, or an absent result for any other type. + /// + /// No conversion is performed. In C, check `is_ok` before reading `ok`. + /// `NaN` is a number and produces a present result. + #[must_use] + pub fn as_number(&self) -> DiplomatOption { + self.0.as_number().into() + } + } + #[cfg(test)] mod tests { use super::{BoaContext, Context}; From 4f3e959167f79a97eeb368a08248449980dc9abf Mon Sep 17 00:00:00 2001 From: Roger Chen Date: Wed, 21 Oct 2026 12:00:00 -0400 Subject: [PATCH 06/11] Add Rust value and GC retention tests --- ffi/boa_ffi/src/lib.rs | 100 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/ffi/boa_ffi/src/lib.rs b/ffi/boa_ffi/src/lib.rs index bdefdc559f5..bc3934a34cb 100644 --- a/ffi/boa_ffi/src/lib.rs +++ b/ffi/boa_ffi/src/lib.rs @@ -121,7 +121,8 @@ pub mod ffi { #[cfg(test)] mod tests { - use super::{BoaContext, Context}; + use super::{BoaContext, BoaJsValue, Context, JsValue, JsValueType}; + use boa_engine::{Source, gc, js_string}; #[test] fn initialization_failure_returns_none() { @@ -154,5 +155,102 @@ pub mod ffi { .expect("all nonblocking contexts should have been released"); drop(blocking); } + + #[test] + fn numbers_round_trip_without_coercion() { + for number in [ + 0.0, + -0.0, + 7.0, + -7.25, + f64::from(i32::MAX), + f64::from(i32::MAX) + 1.0, + f64::from(i32::MIN), + f64::from(i32::MIN) - 1.0, + 9_007_199_254_740_991.0, + f64::MAX, + f64::MIN_POSITIVE, + f64::from_bits(1), + f64::INFINITY, + f64::NEG_INFINITY, + f64::NAN, + ] { + let value = JsValue::from_number(number); + assert_eq!(value.get_type(), JsValueType::Number); + let actual = value.as_number().into_option().expect("number is present"); + if number.is_nan() { + assert!(actual.is_nan()); + } else { + assert_eq!(actual.to_bits(), number.to_bits(), "{number:?}"); + } + } + } + + #[test] + fn null_and_undefined_are_distinct_non_numbers() { + for (value, expected_type) in [ + (JsValue::from_null(), JsValueType::Null), + (JsValue::from_undefined(), JsValueType::Undefined), + ] { + assert_eq!(value.get_type(), expected_type); + assert!(value.as_number().into_option().is_none()); + } + } + + #[test] + fn type_query_handles_every_boa_value_type() { + let mut context = Context::builder() + .build() + .expect("context should initialize"); + for (source, expected_type) in [ + ("undefined", JsValueType::Undefined), + ("null", JsValueType::Null), + ("true", JsValueType::Boolean), + ("7", JsValueType::Number), + ("'text'", JsValueType::String), + ("Symbol('test')", JsValueType::Symbol), + ("7n", JsValueType::BigInt), + ("({})", JsValueType::Object), + ("(() => 7)", JsValueType::Object), + ] { + let value = JsValue( + context + .eval(Source::from_bytes(source)) + .expect("valid source"), + ); + assert_eq!(value.get_type(), expected_type, "{source}"); + assert_eq!( + value.as_number().into_option().is_some(), + expected_type == JsValueType::Number, + "{source}" + ); + } + } + + #[test] + fn retained_object_keeps_its_identity_and_contents() { + let mut context = Context::builder() + .build() + .expect("context should initialize"); + let original = JsValue( + context + .eval(Source::from_bytes("({ payload: 7 })")) + .expect("object should be created"), + ); + let retained = Box::new(JsValue(original.0.clone())); + assert!(BoaJsValue::same_value(&original.0, &retained.0)); + drop(original); + gc::force_collect(); + + assert_eq!(retained.get_type(), JsValueType::Object); + assert!(retained.as_number().into_option().is_none()); + let payload = retained + .0 + .as_object() + .expect("object should be retained") + .get(js_string!("payload"), &mut context) + .expect("property should be readable"); + assert_eq!(payload.as_number(), Some(7.0)); + } } } From 8f6af8d40d70ce4fc3beafcf74d72434b4b1e638 Mon Sep 17 00:00:00 2001 From: Jason Milad Date: Fri, 23 Oct 2026 12:00:00 -0400 Subject: [PATCH 07/11] Add C value tests and ownership docs --- ffi/boa_ffi/README.md | 102 ++++++++++++++++++++++++++++++++++++ ffi/boa_ffi/c/tests/run.sh | 12 +++-- ffi/boa_ffi/c/tests/value.c | 69 ++++++++++++++++++++++++ ffi/boa_ffi/src/lib.rs | 1 + 4 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 ffi/boa_ffi/README.md create mode 100644 ffi/boa_ffi/c/tests/value.c diff --git a/ffi/boa_ffi/README.md b/ffi/boa_ffi/README.md new file mode 100644 index 00000000000..516c4e7109e --- /dev/null +++ b/ffi/boa_ffi/README.md @@ -0,0 +1,102 @@ +# Boa C bindings + +`boa_ffi` exposes Boa through opaque C handles generated by Diplomat. The current +API supports context lifecycle and value handles with type inspection and +number/null/undefined construction. It does not yet execute JavaScript. The API +is experimental and may change. + +## Build and test + +Use the Rust toolchain required by the workspace. From the repository root: + +```sh +cargo build --locked -p boa_ffi +cargo test --locked -p boa_ffi +./ffi/boa_ffi/c/tests/run.sh +``` + +The library is `target/debug/libboa_ffi.dylib` on macOS or +`target/debug/libboa_ffi.so` on Linux with Cargo's default configuration. Include +`c/generated/BoaContext.h` and `c/generated/JsValue.h` as needed in C programs and +link against this library. + +The C test runner requires a native macOS or Linux environment and a C11 compiler +(`cc`, or an executable specified by `CC`). It builds for the Rust host explicitly, +honors `CARGO_TARGET_DIR`, and runs against that build's library. Its library is in +`target//debug/` by default. Windows support is not covered by this runner. + +Rust tests cover initialization failure, context cleanup, value types, numeric +round trips and object retention across garbage collection. The C tests exercise +context/value lifecycle and numeric edge cases through the generated headers +and shared library. They check results explicitly, so defining `NDEBUG` does not +disable the tests. + +## Regenerate headers + +```sh +cargo run --locked -p diplomat-gen +``` + +The workspace pins Diplomat and `diplomat-tool` to 0.15.0, with +`diplomat-runtime` 0.15.1. `Cargo.lock` also fixes their transitive dependencies; +no globally installed `diplomat-tool` is needed. Regeneration replaces all of +`c/generated/`, including removing obsolete headers. Do not edit those files by +hand. Include regenerated headers with changes to the Rust bridge. + +## Context ownership + +```c +#include "BoaContext.h" + +int main(void) { + BoaContext *context = BoaContext_new(); + if (context == NULL) { + return 1; + } + BoaContext_destroy(context); + return 0; +} +``` + +- `BoaContext_new()` returns one owned handle, or `NULL` on an ordinary engine + initialization error. It uses Boa's fallible context builder. +- Destroy each successful result exactly once with `BoaContext_destroy()`. + Its argument must be a live, non-null context pointer returned by this API. + `BoaContext_destroy(NULL)` is invalid. Do not use C's `free()` on a handle. +- Copying a pointer creates an alias, not another owned handle. After destruction, + every alias is invalid. +- Use and destroy a context on the thread that created it. Calls on the same + handle must not overlap. Multiple independent contexts can coexist on one thread. +- `NULL` does not represent recovery from arbitrary Rust panics or allocation + failure. These failures may abort the process; this API does not catch panics. + +## Value handles + +`JsValue_from_number(double)`, `JsValue_from_null()` and +`JsValue_from_undefined()` each return one owned, non-null value handle. They +require no context, and these primitive values remain usable after a context +is destroyed. Allocation failures and unexpected Rust panics may abort the process. + +```c +JsValue *value = JsValue_from_number(7.0); +JsValue_as_number_result number = JsValue_as_number(value); +if (number.is_ok) { + printf("%g\n", number.ok); +} +JsValue_destroy(value); +``` + +`JsValue_get_type()` borrows the handle and returns its `JsValueType`. It reports +the ECMAScript language type: null and undefined are distinct, and functions +have the object type. This differs from JavaScript's `typeof` operator. +The underlying handle retains the original Boa value, including object identity. + +`JsValue_as_number()` borrows the handle and returns a tagged optional double. +Check `is_ok` before reading `ok`; only number values have a numeric payload. +Null and undefined produce an absent result without coercion. NaN is a present +number, as are infinities and signed zeros. NaN payload bits are not guaranteed. + +All value calls, including destruction, must stay on the creating thread. Getters +require a live, non-null value handle and do not consume it. Destroy each owned +handle exactly once with `JsValue_destroy()`, and invalidate every pointer alias +afterward. `JsValue_destroy(NULL)` and C's `free()` are invalid for these handles. diff --git a/ffi/boa_ffi/c/tests/run.sh b/ffi/boa_ffi/c/tests/run.sh index cfac0a5ba0f..0bcb49d7a4b 100755 --- a/ffi/boa_ffi/c/tests/run.sh +++ b/ffi/boa_ffi/c/tests/run.sh @@ -21,8 +21,10 @@ library_dir=$(CDPATH= cd -- "$CARGO_TARGET_DIR/$host/debug" && pwd) test_dir=$(mktemp -d "${TMPDIR:-/tmp}/boa-ffi-c.XXXXXX") trap 'rm -rf "$test_dir"' EXIT HUP INT TERM -"${CC:-cc}" -std=c11 -Wall -Wextra -Werror -pedantic \ - -I "$script_dir/../generated" "$script_dir/context.c" \ - "$library_dir/$library_name" "-Wl,-rpath,$library_dir" \ - -o "$test_dir/context" -"$test_dir/context" +for test_name in context value; do + "${CC:-cc}" -std=c11 -Wall -Wextra -Werror -pedantic \ + -I "$script_dir/../generated" "$script_dir/$test_name.c" \ + "$library_dir/$library_name" "-Wl,-rpath,$library_dir" \ + -o "$test_dir/$test_name" + "$test_dir/$test_name" +done diff --git a/ffi/boa_ffi/c/tests/value.c b/ffi/boa_ffi/c/tests/value.c new file mode 100644 index 00000000000..c6fb1ad0688 --- /dev/null +++ b/ffi/boa_ffi/c/tests/value.c @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include + +#include "BoaContext.h" +#include "JsValue.h" + +static bool same_number(double actual, double expected) { + if (isnan(expected)) { + return isnan(actual); + } + if (actual != expected) { + return false; + } + return expected != 0.0 || (signbit(actual) != 0) == (signbit(expected) != 0); +} + +int main(void) { + const double numbers[] = { + 0.0, -0.0, 1.5, -2.75, + -2147483649.0, -2147483648.0, 2147483647.0, 2147483648.0, + 9007199254740991.0, NAN, INFINITY, -INFINITY, + DBL_MIN, DBL_TRUE_MIN, DBL_MAX, -DBL_MAX, + }; + + for (size_t i = 0; i < sizeof(numbers) / sizeof(numbers[0]); ++i) { + JsValue *value = JsValue_from_number(numbers[i]); + for (size_t read = 0; read < 2; ++read) { + JsValue_as_number_result result = JsValue_as_number(value); + if (JsValue_get_type(value) != JsValueType_Number || + !result.is_ok || !same_number(result.ok, numbers[i])) { + JsValue_destroy(value); + fprintf(stderr, "number round trip failed at index %zu\n", i); + return EXIT_FAILURE; + } + } + JsValue_destroy(value); + } + + BoaContext *context = BoaContext_new(); + if (context == NULL) { + fputs("context failed to initialize\n", stderr); + return EXIT_FAILURE; + } + JsValue *null_value = JsValue_from_null(); + JsValue *undefined_value = JsValue_from_undefined(); + JsValue *number = JsValue_from_number(42.5); + BoaContext_destroy(context); + + JsValue_as_number_result result = JsValue_as_number(number); + bool valid = JsValue_get_type(null_value) == JsValueType_Null && + JsValue_get_type(undefined_value) == JsValueType_Undefined && + !JsValue_as_number(null_value).is_ok && + !JsValue_as_number(undefined_value).is_ok && + JsValue_get_type(number) == JsValueType_Number && + result.is_ok && same_number(result.ok, 42.5); + JsValue_destroy(null_value); + JsValue_destroy(undefined_value); + JsValue_destroy(number); + if (!valid) { + fputs("primitive values failed after context destruction\n", stderr); + return EXIT_FAILURE; + } + + puts("C value tests passed."); + return EXIT_SUCCESS; +} diff --git a/ffi/boa_ffi/src/lib.rs b/ffi/boa_ffi/src/lib.rs index bc3934a34cb..8bc054cf2fb 100644 --- a/ffi/boa_ffi/src/lib.rs +++ b/ffi/boa_ffi/src/lib.rs @@ -1,5 +1,6 @@ //! C bindings for the Boa JavaScript engine. //! +//! The C API and its ownership rules are documented in the crate's README. //! Rust applications should use `boa_engine` directly. /// Entry points used by the generated C bindings. From 2e660ce884dc8eca7b3e212667d4346e92519f2e Mon Sep 17 00:00:00 2001 From: Roger Chen Date: Sun, 25 Oct 2026 15:44:24 -0400 Subject: [PATCH 08/11] Add structured FFI errors --- .github/workflows/rust.yml | 11 + ffi/boa_ffi/README.md | 80 ++++- ffi/boa_ffi/c/generated/JsError.d.h | 19 ++ ffi/boa_ffi/c/generated/JsError.h | 39 +++ .../c/generated/JsErrorRepresentation.d.h | 24 ++ .../c/generated/JsErrorRepresentation.h | 22 ++ ffi/boa_ffi/c/generated/JsNativeErrorInfo.d.h | 24 ++ ffi/boa_ffi/c/generated/JsNativeErrorInfo.h | 22 ++ ffi/boa_ffi/c/generated/JsNativeErrorKind.d.h | 30 ++ ffi/boa_ffi/c/generated/JsNativeErrorKind.h | 22 ++ ffi/boa_ffi/c/tests/error.c | 110 +++++++ ffi/boa_ffi/c/tests/run.sh | 2 +- ffi/boa_ffi/src/lib.rs | 309 +++++++++++++++++- 13 files changed, 691 insertions(+), 23 deletions(-) create mode 100644 ffi/boa_ffi/c/generated/JsError.d.h create mode 100644 ffi/boa_ffi/c/generated/JsError.h create mode 100644 ffi/boa_ffi/c/generated/JsErrorRepresentation.d.h create mode 100644 ffi/boa_ffi/c/generated/JsErrorRepresentation.h create mode 100644 ffi/boa_ffi/c/generated/JsNativeErrorInfo.d.h create mode 100644 ffi/boa_ffi/c/generated/JsNativeErrorInfo.h create mode 100644 ffi/boa_ffi/c/generated/JsNativeErrorKind.d.h create mode 100644 ffi/boa_ffi/c/generated/JsNativeErrorKind.h create mode 100644 ffi/boa_ffi/c/tests/error.c diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b16cd41064f..78e8fd67e7e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -262,6 +262,17 @@ jobs: - name: Test bytecode output run: cargo insta test -p insta-bytecode + - name: Check generated C headers + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + cargo run --locked -p diplomat-gen + git status --short --untracked-files=all -- ffi/boa_ffi/c/generated/ + test -z "$(git status --porcelain --untracked-files=all -- ffi/boa_ffi/c/generated/)" + + - name: Test C bindings + if: ${{ matrix.os != 'windows-latest' }} + run: ./ffi/boa_ffi/c/tests/run.sh + cross-tests: name: Test runs-on: ubuntu-latest diff --git a/ffi/boa_ffi/README.md b/ffi/boa_ffi/README.md index 516c4e7109e..36cd443ae3a 100644 --- a/ffi/boa_ffi/README.md +++ b/ffi/boa_ffi/README.md @@ -2,8 +2,8 @@ `boa_ffi` exposes Boa through opaque C handles generated by Diplomat. The current API supports context lifecycle and value handles with type inspection and -number/null/undefined construction. It does not yet execute JavaScript. The API -is experimental and may change. +number/null/undefined construction, plus structured error construction and +inspection. It does not yet execute JavaScript. The API is experimental and may change. ## Build and test @@ -17,8 +17,8 @@ cargo test --locked -p boa_ffi The library is `target/debug/libboa_ffi.dylib` on macOS or `target/debug/libboa_ffi.so` on Linux with Cargo's default configuration. Include -`c/generated/BoaContext.h` and `c/generated/JsValue.h` as needed in C programs and -link against this library. +`c/generated/BoaContext.h`, `c/generated/JsValue.h` and `c/generated/JsError.h` as +needed in C programs and link against this library. The C test runner requires a native macOS or Linux environment and a C11 compiler (`cc`, or an executable specified by `CC`). It builds for the Rust host explicitly, @@ -26,10 +26,12 @@ honors `CARGO_TARGET_DIR`, and runs against that build's library. Its library is `target//debug/` by default. Windows support is not covered by this runner. Rust tests cover initialization failure, context cleanup, value types, numeric -round trips and object retention across garbage collection. The C tests exercise -context/value lifecycle and numeric edge cases through the generated headers -and shared library. They check results explicitly, so defining `NDEBUG` does not -disable the tests. +round trips, retained error data and inspection without JavaScript execution. +Object retention is checked across garbage collection. The C tests exercise +context/value/error lifecycle, numeric edge cases and message encoding through +the generated headers and shared library. They check results explicitly, so +defining `NDEBUG` does not disable the tests. The existing CI workflow runs the +C tests on its native macOS/Linux runners and checks header regeneration on Linux. ## Regenerate headers @@ -100,3 +102,65 @@ All value calls, including destruction, must stay on the creating thread. Getter require a live, non-null value handle and do not consume it. Destroy each owned handle exactly once with `JsValue_destroy()`, and invalidate every pointer alias afterward. `JsValue_destroy(NULL)` and C's `free()` are invalid for these handles. + +## Errors + +`JsError` retains Boa's original error and its stored representation: + +- `Native` has a native category and UTF-8 message. +- `Opaque` contains an arbitrary thrown value, including JavaScript Error objects. +- `Engine` is an engine failure that JavaScript cannot catch. + +`JsError_get_representation()` and the getters inspect stored data without running +JavaScript. They do not read object properties or normalize opaque Error objects +into native errors. Engine details, cause/aggregate traversal and normalization +are not exposed yet; their underlying data remains in the error handle. + +`JsError_from_native()` copies the supplied UTF-8 message, using its explicit byte +length, including any embedded NUL. It requires no context and returns an owned +handle for each known category. Aggregate construction starts with no members. +The reserved `Other` category is for inspecting unrecognized native categories; +passing it to the constructor returns `NULL`. Allocation failures and unexpected +panics may abort. Message inputs must be valid UTF-8; invalid encodings, invalid +enum discriminants and invalid pointers violate the caller's contract. + +```c +#include +#include "JsError.h" + +int main(void) { + const char message[] = "invalid input"; + JsError *error = JsError_from_native(JsNativeErrorKind_Type, + (DiplomatStringView){.data = message, .len = sizeof(message) - 1}); + if (error == NULL) { + return 1; + } + JsError_as_native_result info = JsError_as_native(error); + if (!info.is_ok) { + JsError_destroy(error); + return 1; + } + fwrite(info.ok.message.data, 1, info.ok.message.len, stdout); + fputc('\n', stdout); + JsError_destroy(error); + return 0; +} +``` + +`JsError_as_native()` returns a tagged optional category/message pair. Check +`is_ok` before reading `ok`; an empty message is present, while an opaque or engine +error has no native details. The message bytes are borrowed from the error and +remain valid until that error is destroyed. Use the byte length, not `strlen()` +or `%s`. Do not free the message or retain its view after destroying the error. + +`JsError_from_opaque()` borrows a value and retains a clone; the caller keeps the +input handle and may destroy it independently. `JsError_as_opaque()` returns a +new owned value handle for the stored payload, or `NULL` for native/engine errors. +Null and undefined payloads return present handles with their respective types. +Each returned payload must be destroyed with `JsValue_destroy()`, independently +of the error. Object identity is preserved. Keep a context alive while using +resources that depend on it, including retained objects. + +Use and destroy errors on their creating thread. Getters require live, non-null +handles. Destroy each owned error exactly once with `JsError_destroy()`; pointer +aliases then become invalid. `JsError_destroy(NULL)` and C's `free()` are invalid. diff --git a/ffi/boa_ffi/c/generated/JsError.d.h b/ffi/boa_ffi/c/generated/JsError.d.h new file mode 100644 index 00000000000..940324001f8 --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsError.d.h @@ -0,0 +1,19 @@ +#ifndef JsError_D_H +#define JsError_D_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + + + + +typedef struct JsError JsError; + + + + +#endif // JsError_D_H diff --git a/ffi/boa_ffi/c/generated/JsError.h b/ffi/boa_ffi/c/generated/JsError.h new file mode 100644 index 00000000000..71b49be78bd --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsError.h @@ -0,0 +1,39 @@ +#ifndef JsError_H +#define JsError_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + +#include "JsErrorRepresentation.d.h" +#include "JsNativeErrorInfo.d.h" +#include "JsNativeErrorKind.d.h" +#include "JsValue.d.h" + +#include "JsError.d.h" + + + + + + +JsError* JsError_from_native(JsNativeErrorKind kind, DiplomatStringView message); + +JsError* JsError_from_opaque(const JsValue* value); + +JsErrorRepresentation JsError_get_representation(const JsError* self); + +typedef struct JsError_as_native_result {union {JsNativeErrorInfo ok; }; bool is_ok;} JsError_as_native_result; +JsError_as_native_result JsError_as_native(const JsError* self); + +JsValue* JsError_as_opaque(const JsError* self); + +void JsError_destroy(JsError* self); + + + + + +#endif // JsError_H diff --git a/ffi/boa_ffi/c/generated/JsErrorRepresentation.d.h b/ffi/boa_ffi/c/generated/JsErrorRepresentation.d.h new file mode 100644 index 00000000000..56528795aac --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsErrorRepresentation.d.h @@ -0,0 +1,24 @@ +#ifndef JsErrorRepresentation_D_H +#define JsErrorRepresentation_D_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + + + + +typedef enum JsErrorRepresentation { + JsErrorRepresentation_Native = 0, + JsErrorRepresentation_Opaque = 1, + JsErrorRepresentation_Engine = 2, +} JsErrorRepresentation; + +typedef struct JsErrorRepresentation_option {union { JsErrorRepresentation ok; }; bool is_ok; } JsErrorRepresentation_option; + + + +#endif // JsErrorRepresentation_D_H diff --git a/ffi/boa_ffi/c/generated/JsErrorRepresentation.h b/ffi/boa_ffi/c/generated/JsErrorRepresentation.h new file mode 100644 index 00000000000..5d1d6060792 --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsErrorRepresentation.h @@ -0,0 +1,22 @@ +#ifndef JsErrorRepresentation_H +#define JsErrorRepresentation_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + +#include "JsErrorRepresentation.d.h" + + + + +// No Content + + + + + +#endif // JsErrorRepresentation_H diff --git a/ffi/boa_ffi/c/generated/JsNativeErrorInfo.d.h b/ffi/boa_ffi/c/generated/JsNativeErrorInfo.d.h new file mode 100644 index 00000000000..d99823f2071 --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsNativeErrorInfo.d.h @@ -0,0 +1,24 @@ +#ifndef JsNativeErrorInfo_D_H +#define JsNativeErrorInfo_D_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + +#include "JsNativeErrorKind.d.h" + + + + +typedef struct JsNativeErrorInfo { + JsNativeErrorKind kind; + DiplomatStringView message; +} JsNativeErrorInfo; + +typedef struct JsNativeErrorInfo_option {union { JsNativeErrorInfo ok; }; bool is_ok; } JsNativeErrorInfo_option; + + + +#endif // JsNativeErrorInfo_D_H diff --git a/ffi/boa_ffi/c/generated/JsNativeErrorInfo.h b/ffi/boa_ffi/c/generated/JsNativeErrorInfo.h new file mode 100644 index 00000000000..033b902777a --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsNativeErrorInfo.h @@ -0,0 +1,22 @@ +#ifndef JsNativeErrorInfo_H +#define JsNativeErrorInfo_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + +#include "JsNativeErrorInfo.d.h" + + + + +// No Content + + + + + +#endif // JsNativeErrorInfo_H diff --git a/ffi/boa_ffi/c/generated/JsNativeErrorKind.d.h b/ffi/boa_ffi/c/generated/JsNativeErrorKind.d.h new file mode 100644 index 00000000000..7f3078d3b10 --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsNativeErrorKind.d.h @@ -0,0 +1,30 @@ +#ifndef JsNativeErrorKind_D_H +#define JsNativeErrorKind_D_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + + + + +typedef enum JsNativeErrorKind { + JsNativeErrorKind_Error = 0, + JsNativeErrorKind_Eval = 1, + JsNativeErrorKind_Range = 2, + JsNativeErrorKind_Reference = 3, + JsNativeErrorKind_Syntax = 4, + JsNativeErrorKind_Type = 5, + JsNativeErrorKind_Uri = 6, + JsNativeErrorKind_Aggregate = 7, + JsNativeErrorKind_Other = 8, +} JsNativeErrorKind; + +typedef struct JsNativeErrorKind_option {union { JsNativeErrorKind ok; }; bool is_ok; } JsNativeErrorKind_option; + + + +#endif // JsNativeErrorKind_D_H diff --git a/ffi/boa_ffi/c/generated/JsNativeErrorKind.h b/ffi/boa_ffi/c/generated/JsNativeErrorKind.h new file mode 100644 index 00000000000..dc99c54e0e9 --- /dev/null +++ b/ffi/boa_ffi/c/generated/JsNativeErrorKind.h @@ -0,0 +1,22 @@ +#ifndef JsNativeErrorKind_H +#define JsNativeErrorKind_H + +#include +#include +#include +#include +#include "diplomat_runtime.h" + + +#include "JsNativeErrorKind.d.h" + + + + +// No Content + + + + + +#endif // JsNativeErrorKind_H diff --git a/ffi/boa_ffi/c/tests/error.c b/ffi/boa_ffi/c/tests/error.c new file mode 100644 index 00000000000..a3dffeea3bb --- /dev/null +++ b/ffi/boa_ffi/c/tests/error.c @@ -0,0 +1,110 @@ +#include +#include +#include +#include + +#include "JsError.h" +#include "JsValue.h" + +static bool native_errors(void) { + const JsNativeErrorKind kinds[] = { + JsNativeErrorKind_Error, JsNativeErrorKind_Eval, + JsNativeErrorKind_Range, JsNativeErrorKind_Reference, + JsNativeErrorKind_Syntax, JsNativeErrorKind_Type, + JsNativeErrorKind_Uri, JsNativeErrorKind_Aggregate, + }; + const char expected[] = "bad \xF0\x9F\xA6\x80\0tail"; + const size_t lengths[] = {0, 3, sizeof(expected) - 1}; + + for (size_t i = 0; i < sizeof(kinds) / sizeof(kinds[0]); ++i) { + for (size_t j = 0; j < sizeof(lengths) / sizeof(lengths[0]); ++j) { + char message[sizeof(expected)]; + memcpy(message, expected, sizeof(message)); + JsError *error = JsError_from_native(kinds[i], + (DiplomatStringView){.data = message, .len = lengths[j]}); + if (error == NULL) { + return false; + } + memset(message, 'x', sizeof(message)); + + JsError_as_native_result info = JsError_as_native(error); + JsValue *payload = JsError_as_opaque(error); + bool valid = JsError_get_representation(error) == JsErrorRepresentation_Native && + info.is_ok && info.ok.kind == kinds[i] && + info.ok.message.len == lengths[j] && + (lengths[j] == 0 || + memcmp(info.ok.message.data, expected, lengths[j]) == 0) && + payload == NULL; + if (payload != NULL) { + JsValue_destroy(payload); + } + JsError_destroy(error); + if (!valid) { + return false; + } + } + } + + JsError *other = JsError_from_native(JsNativeErrorKind_Other, + (DiplomatStringView){.data = "", .len = 0}); + if (other != NULL) { + JsError_destroy(other); + return false; + } + return true; +} + +static bool payload_matches(JsValue *value, JsValueType kind) { + if (value == NULL || JsValue_get_type(value) != kind) { + return false; + } + JsValue_as_number_result number = JsValue_as_number(value); + return kind == JsValueType_Number + ? number.is_ok && number.ok == 7.0 + : !number.is_ok; +} + +static bool opaque_error(JsValue *input, JsValueType kind, bool error_first) { + JsError *error = JsError_from_opaque(input); + JsValue_destroy(input); + + bool valid = JsError_get_representation(error) == JsErrorRepresentation_Opaque && + !JsError_as_native(error).is_ok; + JsValue *first = JsError_as_opaque(error); + JsValue *second = JsError_as_opaque(error); + if (error_first) { + JsError_destroy(error); + } + + valid = valid && payload_matches(first, kind) && payload_matches(second, kind); + if (first != NULL) { + JsValue_destroy(first); + } + valid = valid && payload_matches(second, kind); + if (second != NULL) { + JsValue_destroy(second); + } + if (!error_first) { + valid = valid && JsError_get_representation(error) == JsErrorRepresentation_Opaque; + JsError_destroy(error); + } + return valid; +} + +int main(void) { + if (!native_errors()) { + fputs("native error details or message ownership failed\n", stderr); + return EXIT_FAILURE; + } + for (size_t order = 0; order < 2; ++order) { + bool error_first = order == 0; + if (!opaque_error(JsValue_from_number(7.0), JsValueType_Number, error_first) || + !opaque_error(JsValue_from_null(), JsValueType_Null, error_first) || + !opaque_error(JsValue_from_undefined(), JsValueType_Undefined, error_first)) { + fputs("opaque error payload ownership failed\n", stderr); + return EXIT_FAILURE; + } + } + puts("C error tests passed."); + return EXIT_SUCCESS; +} diff --git a/ffi/boa_ffi/c/tests/run.sh b/ffi/boa_ffi/c/tests/run.sh index 0bcb49d7a4b..7e2cc496429 100755 --- a/ffi/boa_ffi/c/tests/run.sh +++ b/ffi/boa_ffi/c/tests/run.sh @@ -21,7 +21,7 @@ library_dir=$(CDPATH= cd -- "$CARGO_TARGET_DIR/$host/debug" && pwd) test_dir=$(mktemp -d "${TMPDIR:-/tmp}/boa-ffi-c.XXXXXX") trap 'rm -rf "$test_dir"' EXIT HUP INT TERM -for test_name in context value; do +for test_name in context value error; do "${CC:-cc}" -std=c11 -Wall -Wextra -Werror -pedantic \ -I "$script_dir/../generated" "$script_dir/$test_name.c" \ "$library_dir/$library_name" "-Wl,-rpath,$library_dir" \ diff --git a/ffi/boa_ffi/src/lib.rs b/ffi/boa_ffi/src/lib.rs index 8bc054cf2fb..cc34c3284fb 100644 --- a/ffi/boa_ffi/src/lib.rs +++ b/ffi/boa_ffi/src/lib.rs @@ -10,8 +10,12 @@ reason = "Diplomat transfers opaque handle ownership to C through boxed pointers" )] pub mod ffi { - use boa_engine::{Context, JsValue as BoaJsValue, context::ContextBuilder, value::Type}; - use diplomat_runtime::DiplomatOption; + use boa_engine::{ + Context, JsError as BoaJsError, JsNativeError as BoaJsNativeError, + JsNativeErrorKind as BoaJsNativeErrorKind, JsValue as BoaJsValue, context::ContextBuilder, + value::Type, + }; + use diplomat_runtime::{DiplomatOption, DiplomatUtf8StrSlice}; /// An owned Boa context, exposed as an opaque C handle. /// @@ -48,6 +52,7 @@ pub mod ffi { /// Unlike JavaScript's `typeof`, this distinguishes null from objects. /// Functions have the object type. #[derive(Debug, PartialEq, Eq)] + #[diplomat::enum_convert(Type)] pub enum JsValueType { /// The undefined type. Undefined, @@ -98,16 +103,7 @@ pub mod ffi { /// Returns the language type without conversion or JavaScript execution. #[must_use] pub fn get_type(&self) -> JsValueType { - match self.0.get_type() { - Type::Undefined => JsValueType::Undefined, - Type::Null => JsValueType::Null, - Type::Boolean => JsValueType::Boolean, - Type::Number => JsValueType::Number, - Type::String => JsValueType::String, - Type::Symbol => JsValueType::Symbol, - Type::BigInt => JsValueType::BigInt, - Type::Object => JsValueType::Object, - } + self.0.get_type().into() } /// Returns the number, or an absent result for any other type. @@ -120,10 +116,147 @@ pub mod ffi { } } + /// The stored representation of an error, without normalization. + #[derive(Debug, PartialEq, Eq)] + pub enum JsErrorRepresentation { + /// A native Boa error with a category and message. + Native, + /// An arbitrary thrown JavaScript value, including Error objects. + Opaque, + /// An engine failure that JavaScript cannot catch. + Engine, + } + + /// The category of a native Boa error. + #[derive(Debug, PartialEq, Eq)] + pub enum JsNativeErrorKind { + /// A generic error. + Error, + /// An evaluation error. + Eval, + /// A range error. + Range, + /// A reference error. + Reference, + /// A syntax error. + Syntax, + /// A type error. + Type, + /// A URI error. + Uri, + /// An aggregate error. + Aggregate, + /// A native category not recognized by these bindings; inspection only. + Other, + } + + /// Native error details borrowed from a live error handle. + pub struct JsNativeErrorInfo<'a> { + /// The native category. + pub kind: JsNativeErrorKind, + /// UTF-8 bytes, valid until the error is destroyed. Do not free them. + pub message: DiplomatUtf8StrSlice<'a>, + } + + /// An owned Boa error, retaining its original representation and data. + /// + /// Keep it on its creating thread and destroy it exactly once with + /// `JsError_destroy`. Getters borrow a live, non-null handle. + #[derive(Debug)] + #[diplomat::opaque] + pub struct JsError(BoaJsError); + + impl JsError { + /// Creates a native error, copying the UTF-8 message, including any NULs. + /// + /// Returns null in C for `Other`. Aggregate errors start with no members. + /// No context is required. Allocation failures and panics may abort. + #[must_use] + pub fn from_native(kind: JsNativeErrorKind, message: &str) -> Option> { + let error = match kind { + JsNativeErrorKind::Error => BoaJsNativeError::error(), + JsNativeErrorKind::Eval => BoaJsNativeError::eval(), + JsNativeErrorKind::Range => BoaJsNativeError::range(), + JsNativeErrorKind::Reference => BoaJsNativeError::reference(), + JsNativeErrorKind::Syntax => BoaJsNativeError::syntax(), + JsNativeErrorKind::Type => BoaJsNativeError::typ(), + JsNativeErrorKind::Uri => BoaJsNativeError::uri(), + JsNativeErrorKind::Aggregate => BoaJsNativeError::aggregate(Vec::new()), + JsNativeErrorKind::Other => return None, + }; + let error = error.with_message(message.to_owned()); + Some(Box::new(Self(error.into()))) + } + + /// Retains a borrowed value in a new owned opaque error. + /// + /// The caller keeps the input handle. Object identity is preserved. + #[must_use] + pub fn from_opaque(value: &JsValue) -> Box { + Box::new(Self(BoaJsError::from_opaque(value.0.clone()))) + } + + /// Returns the stored representation without executing JavaScript. + #[must_use] + pub fn get_representation(&self) -> JsErrorRepresentation { + if self.0.as_native().is_some() { + JsErrorRepresentation::Native + } else if self.0.as_opaque().is_some() { + JsErrorRepresentation::Opaque + } else { + JsErrorRepresentation::Engine + } + } + + /// Borrows native details, or returns an absent result for other representations. + /// + /// In C, check `is_ok` before reading `ok`. The message view is valid only + /// while this error is alive. No JavaScript properties are read. + #[must_use] + #[allow( + clippy::elidable_lifetime_names, + reason = "Diplomat 0.15 requires a named lifetime for this borrowed return type" + )] + pub fn as_native<'a>(&'a self) -> Option> { + self.0.as_native().map(|error| JsNativeErrorInfo { + kind: match error.kind() { + BoaJsNativeErrorKind::Error => JsNativeErrorKind::Error, + BoaJsNativeErrorKind::Eval => JsNativeErrorKind::Eval, + BoaJsNativeErrorKind::Range => JsNativeErrorKind::Range, + BoaJsNativeErrorKind::Reference => JsNativeErrorKind::Reference, + BoaJsNativeErrorKind::Syntax => JsNativeErrorKind::Syntax, + BoaJsNativeErrorKind::Type => JsNativeErrorKind::Type, + BoaJsNativeErrorKind::Uri => JsNativeErrorKind::Uri, + BoaJsNativeErrorKind::Aggregate(_) => JsNativeErrorKind::Aggregate, + _ => JsNativeErrorKind::Other, + }, + message: error.message().into(), + }) + } + + /// Returns a new owned handle for an opaque payload, or null in C otherwise. + /// + /// Null and undefined payloads are present. Destroy each returned handle + /// with `JsValue_destroy`, independently of this error. No JavaScript runs. + #[must_use] + pub fn as_opaque(&self) -> Option> { + self.0 + .as_opaque() + .map(|value| Box::new(JsValue(value.clone()))) + } + } + #[cfg(test)] mod tests { - use super::{BoaContext, BoaJsValue, Context, JsValue, JsValueType}; - use boa_engine::{Source, gc, js_string}; + use super::{ + BoaContext, BoaJsError, BoaJsNativeError, BoaJsNativeErrorKind, BoaJsValue, Context, + JsError, JsErrorRepresentation, JsNativeErrorKind, JsValue, JsValueType, + }; + use boa_engine::{ + Source, + error::{EngineError, PanicError, RuntimeLimitError}, + gc, js_string, + }; #[test] fn initialization_failure_returns_none() { @@ -253,5 +386,153 @@ pub mod ffi { .expect("property should be readable"); assert_eq!(payload.as_number(), Some(7.0)); } + + #[test] + fn native_errors_preserve_categories_and_messages() { + for (kind, expected) in [ + (JsNativeErrorKind::Error, BoaJsNativeErrorKind::Error), + (JsNativeErrorKind::Eval, BoaJsNativeErrorKind::Eval), + (JsNativeErrorKind::Range, BoaJsNativeErrorKind::Range), + ( + JsNativeErrorKind::Reference, + BoaJsNativeErrorKind::Reference, + ), + (JsNativeErrorKind::Syntax, BoaJsNativeErrorKind::Syntax), + (JsNativeErrorKind::Type, BoaJsNativeErrorKind::Type), + (JsNativeErrorKind::Uri, BoaJsNativeErrorKind::Uri), + ( + JsNativeErrorKind::Aggregate, + BoaJsNativeErrorKind::Aggregate(Vec::new()), + ), + ] { + for message in ["", "invalid \u{1f980} input", "before\0after"] { + let error = JsError::from_native(kind, message).expect("known category"); + assert_eq!(error.get_representation(), JsErrorRepresentation::Native); + let info = error.as_native().expect("native details"); + assert_eq!(info.kind, kind); + assert_eq!(info.message.as_bytes(), message.as_bytes()); + assert_eq!(error.0.as_native().expect("native error").kind(), &expected); + assert!(error.as_opaque().is_none()); + } + } + assert!(JsError::from_native(JsNativeErrorKind::Other, "reserved").is_none()); + } + + #[test] + fn opaque_error_inspection_retains_objects_without_invoking_getters() { + let mut context = Context::builder() + .build() + .expect("context should initialize"); + for expression in ["new TypeError('boom')", "({})"] { + let source = format!( + "globalThis.inspections = 0; + (() => {{ + const value = {expression}; + Object.defineProperties(value, {{ + name: {{ get() {{ inspections++; throw 1; }} }}, + message: {{ get() {{ inspections++; throw 2; }} }}, + cause: {{ get() {{ inspections++; throw 3; }} }}, + payload: {{ value: 7 }} + }}); + throw value; + }})()" + ); + let thrown = context + .eval(Source::from_bytes(&source)) + .expect_err("source should throw"); + let original = JsValue(thrown.as_opaque().expect("opaque throw").clone()); + let reconstructed = JsError::from_opaque(&original); + assert!(reconstructed.as_native().is_none()); + drop(reconstructed); + + let error = JsError(thrown); + assert_eq!(error.get_representation(), JsErrorRepresentation::Opaque); + assert!(error.as_native().is_none()); + let payload = error.as_opaque().expect("retained payload"); + assert!(BoaJsValue::same_value(&payload.0, &original.0)); + drop(original); + drop(error); + gc::force_collect(); + + let value = payload + .0 + .as_object() + .expect("object should be retained") + .get(js_string!("payload"), &mut context) + .expect("data property should be readable"); + assert_eq!(value.as_number(), Some(7.0)); + let inspections = context + .eval(Source::from_bytes("inspections")) + .expect("counter should be readable"); + assert_eq!(inspections.as_number(), Some(0.0)); + } + } + + #[test] + fn native_errors_retain_causes_and_aggregate_members() { + let mut context = Context::builder() + .build() + .expect("context should initialize"); + let value = context + .eval(Source::from_bytes("({ payload: 7 })")) + .expect("object should be created"); + let cause = BoaJsError::from_opaque(value); + let error = JsError( + BoaJsNativeError::aggregate(vec![cause.clone()]) + .with_message("aggregate") + .with_cause(cause) + .into(), + ); + gc::force_collect(); + + let info = error.as_native().expect("native details"); + assert_eq!(info.kind, JsNativeErrorKind::Aggregate); + assert_eq!(info.message.as_bytes(), b"aggregate"); + assert!(error.as_opaque().is_none()); + let native = error.0.as_native().expect("native error"); + let BoaJsNativeErrorKind::Aggregate(members) = native.kind() else { + panic!("aggregate members should be retained"); + }; + assert_eq!(members.len(), 1); + let cause = native.cause().expect("cause should be retained"); + assert_eq!(cause, &members[0]); + let payload = cause + .as_opaque() + .expect("opaque cause") + .as_object() + .expect("object cause") + .get(js_string!("payload"), &mut context) + .expect("data property should be readable"); + assert_eq!(payload.as_number(), Some(7.0)); + } + + #[test] + fn engine_errors_remain_distinct() { + for original in [ + EngineError::from(RuntimeLimitError::LoopIteration), + EngineError::from(RuntimeLimitError::Recursion), + EngineError::from(RuntimeLimitError::StackSize), + EngineError::from( + PanicError::new("engine panic") + .with_source(BoaJsNativeError::error().with_message("source")), + ), + ] { + let error = JsError(original.clone().into()); + assert_eq!(error.get_representation(), JsErrorRepresentation::Engine); + assert!(error.as_native().is_none()); + assert!(error.as_opaque().is_none()); + assert_eq!(error.0.as_engine(), Some(&original)); + } + } + } +} + +impl std::fmt::Debug for ffi::JsNativeErrorInfo<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message: &str = self.message.into(); + f.debug_struct("JsNativeErrorInfo") + .field("kind", &self.kind) + .field("message", &message) + .finish() } } From fed574aec87164e0194d460175d07e2c1d4906ba Mon Sep 17 00:00:00 2001 From: Roger Chen Date: Mon, 26 Oct 2026 01:49:56 -0400 Subject: [PATCH 09/11] Validate error UTF-8 and reuse CI profile --- .github/workflows/rust.yml | 4 ++-- ffi/boa_ffi/README.md | 13 +++++++------ ffi/boa_ffi/c/tests/error.c | 27 +++++++++++++++++++++++++++ ffi/boa_ffi/c/tests/run.sh | 29 +++++++++++++++++++++++------ ffi/boa_ffi/src/lib.rs | 18 ++++++++++++------ 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 78e8fd67e7e..ce6856aa255 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -265,13 +265,13 @@ jobs: - name: Check generated C headers if: ${{ matrix.os == 'ubuntu-latest' }} run: | - cargo run --locked -p diplomat-gen + cargo run --locked --profile ci -p diplomat-gen git status --short --untracked-files=all -- ffi/boa_ffi/c/generated/ test -z "$(git status --porcelain --untracked-files=all -- ffi/boa_ffi/c/generated/)" - name: Test C bindings if: ${{ matrix.os != 'windows-latest' }} - run: ./ffi/boa_ffi/c/tests/run.sh + run: ./ffi/boa_ffi/c/tests/run.sh --ci cross-tests: name: Test diff --git a/ffi/boa_ffi/README.md b/ffi/boa_ffi/README.md index 36cd443ae3a..1cdac707157 100644 --- a/ffi/boa_ffi/README.md +++ b/ffi/boa_ffi/README.md @@ -21,9 +21,10 @@ The library is `target/debug/libboa_ffi.dylib` on macOS or needed in C programs and link against this library. The C test runner requires a native macOS or Linux environment and a C11 compiler -(`cc`, or an executable specified by `CC`). It builds for the Rust host explicitly, -honors `CARGO_TARGET_DIR`, and runs against that build's library. Its library is in -`target//debug/` by default. Windows support is not covered by this runner. +(`cc`, or an executable specified by `CC`). By default it builds for the Rust +host explicitly in `target//debug/`, honors `CARGO_TARGET_DIR`, and runs +against that build's library; `--ci` reuses Cargo's `ci` profile build. Windows +support is not covered by this runner. Rust tests cover initialization failure, context cleanup, value types, numeric round trips, retained error data and inspection without JavaScript execution. @@ -120,9 +121,9 @@ are not exposed yet; their underlying data remains in the error handle. length, including any embedded NUL. It requires no context and returns an owned handle for each known category. Aggregate construction starts with no members. The reserved `Other` category is for inspecting unrecognized native categories; -passing it to the constructor returns `NULL`. Allocation failures and unexpected -panics may abort. Message inputs must be valid UTF-8; invalid encodings, invalid -enum discriminants and invalid pointers violate the caller's contract. +passing `Other` or malformed UTF-8 to the constructor returns `NULL`. Allocation +failures and unexpected panics may abort. Invalid enum discriminants and invalid +non-empty pointers violate the caller's contract. ```c #include diff --git a/ffi/boa_ffi/c/tests/error.c b/ffi/boa_ffi/c/tests/error.c index a3dffeea3bb..3fc34303a2a 100644 --- a/ffi/boa_ffi/c/tests/error.c +++ b/ffi/boa_ffi/c/tests/error.c @@ -45,6 +45,33 @@ static bool native_errors(void) { } } + JsError *empty = JsError_from_native(JsNativeErrorKind_Error, + (DiplomatStringView){.data = NULL, .len = 0}); + if (empty == NULL) { + return false; + } + JsError_as_native_result empty_info = JsError_as_native(empty); + bool empty_valid = empty_info.is_ok && + empty_info.ok.kind == JsNativeErrorKind_Error && + empty_info.ok.message.len == 0; + JsError_destroy(empty); + if (!empty_valid) { + return false; + } + + const DiplomatStringView rejected_messages[] = { + {.data = "\xFF", .len = 1}, + {.data = expected, .len = 7}, + {.data = "\xED\xA0\x80", .len = 3}, + }; + for (size_t i = 0; i < sizeof(rejected_messages) / sizeof(rejected_messages[0]); ++i) { + JsError *invalid = JsError_from_native(JsNativeErrorKind_Type, rejected_messages[i]); + if (invalid != NULL) { + JsError_destroy(invalid); + return false; + } + } + JsError *other = JsError_from_native(JsNativeErrorKind_Other, (DiplomatStringView){.data = "", .len = 0}); if (other != NULL) { diff --git a/ffi/boa_ffi/c/tests/run.sh b/ffi/boa_ffi/c/tests/run.sh index 7e2cc496429..eb08a000da4 100755 --- a/ffi/boa_ffi/c/tests/run.sh +++ b/ffi/boa_ffi/c/tests/run.sh @@ -11,12 +11,29 @@ case $(uname -s) in *) echo "The C test runner supports native macOS and Linux builds." >&2; exit 1 ;; esac -# Select the host explicitly so Cargo's configured target cannot cause us to -# accidentally link a stale native library after a cross-compilation build. -host=$(rustc -vV | sed -n 's/^host: //p') +if [ "$#" -gt 1 ]; then + echo "Usage: $0 [--ci]" >&2 + exit 2 +fi + export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-"$repo_root/target"}" -cargo build --locked -p boa_ffi --target "$host" -library_dir=$(CDPATH= cd -- "$CARGO_TARGET_DIR/$host/debug" && pwd) +case "${1-}" in + "") + # Select the host explicitly so Cargo's configured target cannot cause us to + # accidentally link a stale native library after a cross-compilation build. + host=$(rustc -vV | sed -n 's/^host: //p') + cargo build --locked -p boa_ffi --target "$host" + library_dir=$(CDPATH= cd -- "$CARGO_TARGET_DIR/$host/debug" && pwd) + ;; + --ci) + cargo build --locked --profile ci -p boa_ffi + library_dir=$(CDPATH= cd -- "$CARGO_TARGET_DIR/ci" && pwd) + ;; + *) + echo "Usage: $0 [--ci]" >&2 + exit 2 + ;; +esac test_dir=$(mktemp -d "${TMPDIR:-/tmp}/boa-ffi-c.XXXXXX") trap 'rm -rf "$test_dir"' EXIT HUP INT TERM @@ -24,7 +41,7 @@ trap 'rm -rf "$test_dir"' EXIT HUP INT TERM for test_name in context value error; do "${CC:-cc}" -std=c11 -Wall -Wextra -Werror -pedantic \ -I "$script_dir/../generated" "$script_dir/$test_name.c" \ - "$library_dir/$library_name" "-Wl,-rpath,$library_dir" \ + "$library_dir/$library_name" "-Wl,-rpath,$library_dir" -lm \ -o "$test_dir/$test_name" "$test_dir/$test_name" done diff --git a/ffi/boa_ffi/src/lib.rs b/ffi/boa_ffi/src/lib.rs index cc34c3284fb..efcee56bc25 100644 --- a/ffi/boa_ffi/src/lib.rs +++ b/ffi/boa_ffi/src/lib.rs @@ -15,7 +15,7 @@ pub mod ffi { JsNativeErrorKind as BoaJsNativeErrorKind, JsValue as BoaJsValue, context::ContextBuilder, value::Type, }; - use diplomat_runtime::{DiplomatOption, DiplomatUtf8StrSlice}; + use diplomat_runtime::{DiplomatOption, DiplomatStr, DiplomatUtf8StrSlice}; /// An owned Boa context, exposed as an opaque C handle. /// @@ -169,10 +169,12 @@ pub mod ffi { impl JsError { /// Creates a native error, copying the UTF-8 message, including any NULs. /// - /// Returns null in C for `Other`. Aggregate errors start with no members. - /// No context is required. Allocation failures and panics may abort. + /// Returns null in C for `Other` or malformed UTF-8. Aggregate errors start + /// with no members. No context is required. Allocation failures and panics + /// may abort. #[must_use] - pub fn from_native(kind: JsNativeErrorKind, message: &str) -> Option> { + pub fn from_native(kind: JsNativeErrorKind, message: &DiplomatStr) -> Option> { + let message = std::str::from_utf8(message).ok()?; let error = match kind { JsNativeErrorKind::Error => BoaJsNativeError::error(), JsNativeErrorKind::Eval => BoaJsNativeError::eval(), @@ -406,7 +408,8 @@ pub mod ffi { ), ] { for message in ["", "invalid \u{1f980} input", "before\0after"] { - let error = JsError::from_native(kind, message).expect("known category"); + let error = + JsError::from_native(kind, message.as_bytes()).expect("known category"); assert_eq!(error.get_representation(), JsErrorRepresentation::Native); let info = error.as_native().expect("native details"); assert_eq!(info.kind, kind); @@ -415,7 +418,10 @@ pub mod ffi { assert!(error.as_opaque().is_none()); } } - assert!(JsError::from_native(JsNativeErrorKind::Other, "reserved").is_none()); + assert!(JsError::from_native(JsNativeErrorKind::Other, b"reserved").is_none()); + for invalid in [&b"\xff"[..], b"bad \xf0\x9f\xa6", b"\xed\xa0\x80"] { + assert!(JsError::from_native(JsNativeErrorKind::Type, invalid).is_none()); + } } #[test] From 26b6a4eee27871e3c098d71cfbd789d5a4782081 Mon Sep 17 00:00:00 2001 From: Roger Chen Date: Sun, 8 Nov 2026 20:52:39 -0500 Subject: [PATCH 10/11] Align FFI layout, CI and Diplomat with temporal_rs --- .github/workflows/rust.yml | 47 +++++++++-- Cargo.lock | 77 +++++++------------ Cargo.toml | 6 +- ffi/boa_ffi/.gitattributes | 1 + ffi/boa_ffi/README.md | 18 ++--- .../generated => bindings/c}/BoaContext.d.h | 0 .../{c/generated => bindings/c}/BoaContext.h | 0 .../{c/generated => bindings/c}/JsError.d.h | 0 .../{c/generated => bindings/c}/JsError.h | 0 .../c}/JsErrorRepresentation.d.h | 0 .../c}/JsErrorRepresentation.h | 0 .../c}/JsNativeErrorInfo.d.h | 0 .../c}/JsNativeErrorInfo.h | 0 .../c}/JsNativeErrorKind.d.h | 0 .../c}/JsNativeErrorKind.h | 0 .../{c/generated => bindings/c}/JsValue.d.h | 0 .../{c/generated => bindings/c}/JsValue.h | 0 .../generated => bindings/c}/JsValueType.d.h | 0 .../{c/generated => bindings/c}/JsValueType.h | 0 .../c}/diplomat_runtime.h | 0 ffi/boa_ffi/{c/tests => tests/c}/context.c | 0 ffi/boa_ffi/{c/tests => tests/c}/error.c | 0 ffi/boa_ffi/{c/tests => tests/c}/run.sh | 2 +- ffi/boa_ffi/{c/tests => tests/c}/value.c | 0 tools/diplomat-gen/src/main.rs | 2 +- 25 files changed, 83 insertions(+), 70 deletions(-) create mode 100644 ffi/boa_ffi/.gitattributes rename ffi/boa_ffi/{c/generated => bindings/c}/BoaContext.d.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/BoaContext.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsError.d.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsError.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsErrorRepresentation.d.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsErrorRepresentation.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsNativeErrorInfo.d.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsNativeErrorInfo.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsNativeErrorKind.d.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsNativeErrorKind.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsValue.d.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsValue.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsValueType.d.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/JsValueType.h (100%) rename ffi/boa_ffi/{c/generated => bindings/c}/diplomat_runtime.h (100%) rename ffi/boa_ffi/{c/tests => tests/c}/context.c (100%) rename ffi/boa_ffi/{c/tests => tests/c}/error.c (100%) rename ffi/boa_ffi/{c/tests => tests/c}/run.sh (95%) rename ffi/boa_ffi/{c/tests => tests/c}/value.c (100%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ce6856aa255..876a6fcd9e8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -262,16 +262,53 @@ jobs: - name: Test bytecode output run: cargo insta test -p insta-bytecode + ffi-tests: + name: Test FFI + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + needs: + - fmt + - typos + - clippy + - docs + - msrv + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + - os: ubuntu-26.04-arm + - os: ubuntu-latest + steps: + - name: Set environment + # Setting `RUSTFLAGS` overrides any flags set on .cargo/config.toml, so we need to + # set the target flags instead which are cumulative. + # Track https://github.com/rust-lang/cargo/issues/5376 + run: | + target=$(rustc -vV | awk '/^host/ { print $2 }' | tr '[:lower:]' '[:upper:]' | tr '-' '_') + echo "CARGO_TARGET_${target}_RUSTFLAGS=$W_FLAGS" >> $GITHUB_ENV + + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: stable + + - name: Cache Cargo + uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + - name: Check generated C headers - if: ${{ matrix.os == 'ubuntu-latest' }} run: | cargo run --locked --profile ci -p diplomat-gen - git status --short --untracked-files=all -- ffi/boa_ffi/c/generated/ - test -z "$(git status --porcelain --untracked-files=all -- ffi/boa_ffi/c/generated/)" + git status --short --untracked-files=all -- ffi/boa_ffi/bindings/c/ + test -z "$(git status --porcelain --untracked-files=all -- ffi/boa_ffi/bindings/c/)" - name: Test C bindings - if: ${{ matrix.os != 'windows-latest' }} - run: ./ffi/boa_ffi/c/tests/run.sh --ci + run: ./ffi/boa_ffi/tests/c/run.sh --ci cross-tests: name: Test diff --git a/Cargo.lock b/Cargo.lock index b5480b9bad7..33027ce8d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "annotate-snippets" +version = "0.12.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a5b0a5ab8561c603f16c3b78c24a386b1f70e7fc1b1b1e8c15738828e4a176" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + [[package]] name = "anstream" version = "1.0.0" @@ -1274,14 +1285,14 @@ dependencies = [ [[package]] name = "diplomat" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7935649d00000f5c5d735448ad3dc07b9738160727017914cf42138b8e8e6611" +checksum = "f8b37a6cea72eb6df5143d3e091d4c34cb0cf4046dd1ddcc082988886fe1f809" dependencies = [ "diplomat_core", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] @@ -1293,46 +1304,44 @@ dependencies = [ [[package]] name = "diplomat-runtime" -version = "0.15.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "970ac38ad677632efcee6d517e783958da9bc78ec206d8d5e35b459ffc5e4864" +checksum = "d9d981121ba76150987fda68185b37853c0b928a55dfdc28897348cd068440be" [[package]] name = "diplomat-tool" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a742eeda95cd614873b815c4577519b07b737e2dceaf65f47d983bbdaabc5d7" +checksum = "f71ad0ab8cf95b7179a192b1e07c3a5e8b7e6471e279d05101a12a1e77d8025c" dependencies = [ "askama", "clap", "colored", "diplomat_core", - "displaydoc", "heck", "indenter", "itertools 0.14.0", - "pulldown-cmark", "quote", "serde", - "syn 2.0.119", + "syn 3.0.9", "syn-inline-mod", "toml 0.8.23", ] [[package]] name = "diplomat_core" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf41b94101a4bce993febaf0098092b0bb31deaf0ecaf6e0a2562465f61b383" +checksum = "73b18e806f9ecb92df828d080efe47984ae1193958295d4a84fb8b32c9a1aa1b" dependencies = [ - "displaydoc", + "annotate-snippets", "either", "proc-macro2", "quote", "serde", "smallvec", "strck", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] @@ -1722,15 +1731,6 @@ dependencies = [ "simple_logger", ] -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -3219,25 +3219,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pulldown-cmark" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" -dependencies = [ - "bitflags", - "getopts", - "memchr", - "pulldown-cmark-escape", - "unicase", -] - -[[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" - [[package]] name = "quote" version = "1.0.47" @@ -3992,12 +3973,12 @@ dependencies = [ [[package]] name = "syn-inline-mod" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa6dca1fdb7b2ed46dd534a326725419d4fb10f23d8c85a8b2860e5eb25d0f9" +checksum = "88d5cd09c54d02e69217d362ee45b62821932cb16778a4513a722364ba4cb098" dependencies = [ "proc-macro2", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] @@ -4533,12 +4514,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" -[[package]] -name = "unicase" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" - [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 10113004442..45e68a5bba1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,9 +58,9 @@ bitflags = "2.13.1" clap = "4.6.6" colored = "3.1.1" cow-utils = "0.1.3" -diplomat = "=0.15.0" -diplomat-runtime = "=0.15.1" -diplomat-tool = "=0.15.0" +diplomat = { version = "0.16.0", default-features = false } +diplomat-runtime = { version = ">= 0.15.2, < 0.17.0", default-features = false } +diplomat-tool = { version = "0.16.0", default-features = false } fast-float2 = "0.2.4" hashbrown = "0.17.1" http = { version = "1.5.0" } diff --git a/ffi/boa_ffi/.gitattributes b/ffi/boa_ffi/.gitattributes new file mode 100644 index 00000000000..49065d790bd --- /dev/null +++ b/ffi/boa_ffi/.gitattributes @@ -0,0 +1 @@ +bindings/** linguist-generated=true diff --git a/ffi/boa_ffi/README.md b/ffi/boa_ffi/README.md index 1cdac707157..7687b4a3ab4 100644 --- a/ffi/boa_ffi/README.md +++ b/ffi/boa_ffi/README.md @@ -12,12 +12,12 @@ Use the Rust toolchain required by the workspace. From the repository root: ```sh cargo build --locked -p boa_ffi cargo test --locked -p boa_ffi -./ffi/boa_ffi/c/tests/run.sh +./ffi/boa_ffi/tests/c/run.sh ``` The library is `target/debug/libboa_ffi.dylib` on macOS or `target/debug/libboa_ffi.so` on Linux with Cargo's default configuration. Include -`c/generated/BoaContext.h`, `c/generated/JsValue.h` and `c/generated/JsError.h` as +`bindings/c/BoaContext.h`, `bindings/c/JsValue.h` and `bindings/c/JsError.h` as needed in C programs and link against this library. The C test runner requires a native macOS or Linux environment and a C11 compiler @@ -31,8 +31,8 @@ round trips, retained error data and inspection without JavaScript execution. Object retention is checked across garbage collection. The C tests exercise context/value/error lifecycle, numeric edge cases and message encoding through the generated headers and shared library. They check results explicitly, so -defining `NDEBUG` does not disable the tests. The existing CI workflow runs the -C tests on its native macOS/Linux runners and checks header regeneration on Linux. +defining `NDEBUG` does not disable the tests. A dedicated CI job runs header +regeneration and the C tests across macOS and Linux. ## Regenerate headers @@ -40,11 +40,11 @@ C tests on its native macOS/Linux runners and checks header regeneration on Linu cargo run --locked -p diplomat-gen ``` -The workspace pins Diplomat and `diplomat-tool` to 0.15.0, with -`diplomat-runtime` 0.15.1. `Cargo.lock` also fixes their transitive dependencies; -no globally installed `diplomat-tool` is needed. Regeneration replaces all of -`c/generated/`, including removing obsolete headers. Do not edit those files by -hand. Include regenerated headers with changes to the Rust bridge. +Diplomat's version requirements live in the workspace `Cargo.toml`, and +`Cargo.lock` fixes the resolved versions, so no globally installed +`diplomat-tool` is needed. Regeneration replaces all of `bindings/c/`, including +removing obsolete headers. Do not edit those files by hand. Include regenerated +headers with changes to the Rust bridge. ## Context ownership diff --git a/ffi/boa_ffi/c/generated/BoaContext.d.h b/ffi/boa_ffi/bindings/c/BoaContext.d.h similarity index 100% rename from ffi/boa_ffi/c/generated/BoaContext.d.h rename to ffi/boa_ffi/bindings/c/BoaContext.d.h diff --git a/ffi/boa_ffi/c/generated/BoaContext.h b/ffi/boa_ffi/bindings/c/BoaContext.h similarity index 100% rename from ffi/boa_ffi/c/generated/BoaContext.h rename to ffi/boa_ffi/bindings/c/BoaContext.h diff --git a/ffi/boa_ffi/c/generated/JsError.d.h b/ffi/boa_ffi/bindings/c/JsError.d.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsError.d.h rename to ffi/boa_ffi/bindings/c/JsError.d.h diff --git a/ffi/boa_ffi/c/generated/JsError.h b/ffi/boa_ffi/bindings/c/JsError.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsError.h rename to ffi/boa_ffi/bindings/c/JsError.h diff --git a/ffi/boa_ffi/c/generated/JsErrorRepresentation.d.h b/ffi/boa_ffi/bindings/c/JsErrorRepresentation.d.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsErrorRepresentation.d.h rename to ffi/boa_ffi/bindings/c/JsErrorRepresentation.d.h diff --git a/ffi/boa_ffi/c/generated/JsErrorRepresentation.h b/ffi/boa_ffi/bindings/c/JsErrorRepresentation.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsErrorRepresentation.h rename to ffi/boa_ffi/bindings/c/JsErrorRepresentation.h diff --git a/ffi/boa_ffi/c/generated/JsNativeErrorInfo.d.h b/ffi/boa_ffi/bindings/c/JsNativeErrorInfo.d.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsNativeErrorInfo.d.h rename to ffi/boa_ffi/bindings/c/JsNativeErrorInfo.d.h diff --git a/ffi/boa_ffi/c/generated/JsNativeErrorInfo.h b/ffi/boa_ffi/bindings/c/JsNativeErrorInfo.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsNativeErrorInfo.h rename to ffi/boa_ffi/bindings/c/JsNativeErrorInfo.h diff --git a/ffi/boa_ffi/c/generated/JsNativeErrorKind.d.h b/ffi/boa_ffi/bindings/c/JsNativeErrorKind.d.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsNativeErrorKind.d.h rename to ffi/boa_ffi/bindings/c/JsNativeErrorKind.d.h diff --git a/ffi/boa_ffi/c/generated/JsNativeErrorKind.h b/ffi/boa_ffi/bindings/c/JsNativeErrorKind.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsNativeErrorKind.h rename to ffi/boa_ffi/bindings/c/JsNativeErrorKind.h diff --git a/ffi/boa_ffi/c/generated/JsValue.d.h b/ffi/boa_ffi/bindings/c/JsValue.d.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsValue.d.h rename to ffi/boa_ffi/bindings/c/JsValue.d.h diff --git a/ffi/boa_ffi/c/generated/JsValue.h b/ffi/boa_ffi/bindings/c/JsValue.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsValue.h rename to ffi/boa_ffi/bindings/c/JsValue.h diff --git a/ffi/boa_ffi/c/generated/JsValueType.d.h b/ffi/boa_ffi/bindings/c/JsValueType.d.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsValueType.d.h rename to ffi/boa_ffi/bindings/c/JsValueType.d.h diff --git a/ffi/boa_ffi/c/generated/JsValueType.h b/ffi/boa_ffi/bindings/c/JsValueType.h similarity index 100% rename from ffi/boa_ffi/c/generated/JsValueType.h rename to ffi/boa_ffi/bindings/c/JsValueType.h diff --git a/ffi/boa_ffi/c/generated/diplomat_runtime.h b/ffi/boa_ffi/bindings/c/diplomat_runtime.h similarity index 100% rename from ffi/boa_ffi/c/generated/diplomat_runtime.h rename to ffi/boa_ffi/bindings/c/diplomat_runtime.h diff --git a/ffi/boa_ffi/c/tests/context.c b/ffi/boa_ffi/tests/c/context.c similarity index 100% rename from ffi/boa_ffi/c/tests/context.c rename to ffi/boa_ffi/tests/c/context.c diff --git a/ffi/boa_ffi/c/tests/error.c b/ffi/boa_ffi/tests/c/error.c similarity index 100% rename from ffi/boa_ffi/c/tests/error.c rename to ffi/boa_ffi/tests/c/error.c diff --git a/ffi/boa_ffi/c/tests/run.sh b/ffi/boa_ffi/tests/c/run.sh similarity index 95% rename from ffi/boa_ffi/c/tests/run.sh rename to ffi/boa_ffi/tests/c/run.sh index eb08a000da4..3c6f315bea6 100755 --- a/ffi/boa_ffi/c/tests/run.sh +++ b/ffi/boa_ffi/tests/c/run.sh @@ -40,7 +40,7 @@ trap 'rm -rf "$test_dir"' EXIT HUP INT TERM for test_name in context value error; do "${CC:-cc}" -std=c11 -Wall -Wextra -Werror -pedantic \ - -I "$script_dir/../generated" "$script_dir/$test_name.c" \ + -I "$script_dir/../../bindings/c" "$script_dir/$test_name.c" \ "$library_dir/$library_name" "-Wl,-rpath,$library_dir" -lm \ -o "$test_dir/$test_name" "$test_dir/$test_name" diff --git a/ffi/boa_ffi/c/tests/value.c b/ffi/boa_ffi/tests/c/value.c similarity index 100% rename from ffi/boa_ffi/c/tests/value.c rename to ffi/boa_ffi/tests/c/value.c diff --git a/tools/diplomat-gen/src/main.rs b/tools/diplomat-gen/src/main.rs index a83241b5a8e..17581b47749 100644 --- a/tools/diplomat-gen/src/main.rs +++ b/tools/diplomat-gen/src/main.rs @@ -6,7 +6,7 @@ use diplomat_tool::{DocsUrlGenerator, config::Config}; fn main() -> io::Result<()> { let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../ffi/boa_ffi"); - let headers = crate_dir.join("c/generated"); + let headers = crate_dir.join("bindings/c"); // This directory contains only generated files. Remove obsolete headers too. match fs::remove_dir_all(&headers) { From 520481e1fed277ecbe2f70db26a837b565a4dbb6 Mon Sep 17 00:00:00 2001 From: Roger Chen Date: Sun, 8 Nov 2026 21:59:03 -0500 Subject: [PATCH 11/11] Prefix C symbols with boa_ and forbid unwrap, expect and indexing Match temporal_capi: `#[diplomat::abi_rename = "boa_{0}"]` prefixes every exported C function to avoid symbol clashes with other libraries, and the crate now forbids `unwrap`, `expect` and indexing outside tests, because a panic in an `extern "C"` function aborts the host program. Also drop the stale Diplomat version from a lint reason. --- ffi/boa_ffi/README.md | 52 +++++++++++++------------- ffi/boa_ffi/bindings/c/BoaContext.h | 4 +- ffi/boa_ffi/bindings/c/JsError.h | 14 +++---- ffi/boa_ffi/bindings/c/JsValue.h | 14 +++---- ffi/boa_ffi/src/lib.rs | 15 +++++--- ffi/boa_ffi/tests/c/context.c | 16 ++++---- ffi/boa_ffi/tests/c/error.c | 58 ++++++++++++++--------------- ffi/boa_ffi/tests/c/value.c | 38 +++++++++---------- 8 files changed, 108 insertions(+), 103 deletions(-) diff --git a/ffi/boa_ffi/README.md b/ffi/boa_ffi/README.md index 7687b4a3ab4..bbc55feb6f2 100644 --- a/ffi/boa_ffi/README.md +++ b/ffi/boa_ffi/README.md @@ -52,20 +52,20 @@ headers with changes to the Rust bridge. #include "BoaContext.h" int main(void) { - BoaContext *context = BoaContext_new(); + BoaContext *context = boa_BoaContext_new(); if (context == NULL) { return 1; } - BoaContext_destroy(context); + boa_BoaContext_destroy(context); return 0; } ``` -- `BoaContext_new()` returns one owned handle, or `NULL` on an ordinary engine +- `boa_BoaContext_new()` returns one owned handle, or `NULL` on an ordinary engine initialization error. It uses Boa's fallible context builder. -- Destroy each successful result exactly once with `BoaContext_destroy()`. +- Destroy each successful result exactly once with `boa_BoaContext_destroy()`. Its argument must be a live, non-null context pointer returned by this API. - `BoaContext_destroy(NULL)` is invalid. Do not use C's `free()` on a handle. + `boa_BoaContext_destroy(NULL)` is invalid. Do not use C's `free()` on a handle. - Copying a pointer creates an alias, not another owned handle. After destruction, every alias is invalid. - Use and destroy a context on the thread that created it. Calls on the same @@ -75,34 +75,34 @@ int main(void) { ## Value handles -`JsValue_from_number(double)`, `JsValue_from_null()` and -`JsValue_from_undefined()` each return one owned, non-null value handle. They +`boa_JsValue_from_number(double)`, `boa_JsValue_from_null()` and +`boa_JsValue_from_undefined()` each return one owned, non-null value handle. They require no context, and these primitive values remain usable after a context is destroyed. Allocation failures and unexpected Rust panics may abort the process. ```c -JsValue *value = JsValue_from_number(7.0); -JsValue_as_number_result number = JsValue_as_number(value); +JsValue *value = boa_JsValue_from_number(7.0); +boa_JsValue_as_number_result number = boa_JsValue_as_number(value); if (number.is_ok) { printf("%g\n", number.ok); } -JsValue_destroy(value); +boa_JsValue_destroy(value); ``` -`JsValue_get_type()` borrows the handle and returns its `JsValueType`. It reports +`boa_JsValue_get_type()` borrows the handle and returns its `JsValueType`. It reports the ECMAScript language type: null and undefined are distinct, and functions have the object type. This differs from JavaScript's `typeof` operator. The underlying handle retains the original Boa value, including object identity. -`JsValue_as_number()` borrows the handle and returns a tagged optional double. +`boa_JsValue_as_number()` borrows the handle and returns a tagged optional double. Check `is_ok` before reading `ok`; only number values have a numeric payload. Null and undefined produce an absent result without coercion. NaN is a present number, as are infinities and signed zeros. NaN payload bits are not guaranteed. All value calls, including destruction, must stay on the creating thread. Getters require a live, non-null value handle and do not consume it. Destroy each owned -handle exactly once with `JsValue_destroy()`, and invalidate every pointer alias -afterward. `JsValue_destroy(NULL)` and C's `free()` are invalid for these handles. +handle exactly once with `boa_JsValue_destroy()`, and invalidate every pointer alias +afterward. `boa_JsValue_destroy(NULL)` and C's `free()` are invalid for these handles. ## Errors @@ -112,12 +112,12 @@ afterward. `JsValue_destroy(NULL)` and C's `free()` are invalid for these handle - `Opaque` contains an arbitrary thrown value, including JavaScript Error objects. - `Engine` is an engine failure that JavaScript cannot catch. -`JsError_get_representation()` and the getters inspect stored data without running +`boa_JsError_get_representation()` and the getters inspect stored data without running JavaScript. They do not read object properties or normalize opaque Error objects into native errors. Engine details, cause/aggregate traversal and normalization are not exposed yet; their underlying data remains in the error handle. -`JsError_from_native()` copies the supplied UTF-8 message, using its explicit byte +`boa_JsError_from_native()` copies the supplied UTF-8 message, using its explicit byte length, including any embedded NUL. It requires no context and returns an owned handle for each known category. Aggregate construction starts with no members. The reserved `Other` category is for inspecting unrecognized native categories; @@ -131,37 +131,37 @@ non-empty pointers violate the caller's contract. int main(void) { const char message[] = "invalid input"; - JsError *error = JsError_from_native(JsNativeErrorKind_Type, + JsError *error = boa_JsError_from_native(JsNativeErrorKind_Type, (DiplomatStringView){.data = message, .len = sizeof(message) - 1}); if (error == NULL) { return 1; } - JsError_as_native_result info = JsError_as_native(error); + boa_JsError_as_native_result info = boa_JsError_as_native(error); if (!info.is_ok) { - JsError_destroy(error); + boa_JsError_destroy(error); return 1; } fwrite(info.ok.message.data, 1, info.ok.message.len, stdout); fputc('\n', stdout); - JsError_destroy(error); + boa_JsError_destroy(error); return 0; } ``` -`JsError_as_native()` returns a tagged optional category/message pair. Check +`boa_JsError_as_native()` returns a tagged optional category/message pair. Check `is_ok` before reading `ok`; an empty message is present, while an opaque or engine error has no native details. The message bytes are borrowed from the error and remain valid until that error is destroyed. Use the byte length, not `strlen()` or `%s`. Do not free the message or retain its view after destroying the error. -`JsError_from_opaque()` borrows a value and retains a clone; the caller keeps the -input handle and may destroy it independently. `JsError_as_opaque()` returns a +`boa_JsError_from_opaque()` borrows a value and retains a clone; the caller keeps the +input handle and may destroy it independently. `boa_JsError_as_opaque()` returns a new owned value handle for the stored payload, or `NULL` for native/engine errors. Null and undefined payloads return present handles with their respective types. -Each returned payload must be destroyed with `JsValue_destroy()`, independently +Each returned payload must be destroyed with `boa_JsValue_destroy()`, independently of the error. Object identity is preserved. Keep a context alive while using resources that depend on it, including retained objects. Use and destroy errors on their creating thread. Getters require live, non-null -handles. Destroy each owned error exactly once with `JsError_destroy()`; pointer -aliases then become invalid. `JsError_destroy(NULL)` and C's `free()` are invalid. +handles. Destroy each owned error exactly once with `boa_JsError_destroy()`; pointer +aliases then become invalid. `boa_JsError_destroy(NULL)` and C's `free()` are invalid. diff --git a/ffi/boa_ffi/bindings/c/BoaContext.h b/ffi/boa_ffi/bindings/c/BoaContext.h index a2f582bc66c..17c3e785646 100644 --- a/ffi/boa_ffi/bindings/c/BoaContext.h +++ b/ffi/boa_ffi/bindings/c/BoaContext.h @@ -15,9 +15,9 @@ -BoaContext* BoaContext_new(void); +BoaContext* boa_BoaContext_new(void); -void BoaContext_destroy(BoaContext* self); +void boa_BoaContext_destroy(BoaContext* self); diff --git a/ffi/boa_ffi/bindings/c/JsError.h b/ffi/boa_ffi/bindings/c/JsError.h index 71b49be78bd..bf386bc0227 100644 --- a/ffi/boa_ffi/bindings/c/JsError.h +++ b/ffi/boa_ffi/bindings/c/JsError.h @@ -19,18 +19,18 @@ -JsError* JsError_from_native(JsNativeErrorKind kind, DiplomatStringView message); +JsError* boa_JsError_from_native(JsNativeErrorKind kind, DiplomatStringView message); -JsError* JsError_from_opaque(const JsValue* value); +JsError* boa_JsError_from_opaque(const JsValue* value); -JsErrorRepresentation JsError_get_representation(const JsError* self); +JsErrorRepresentation boa_JsError_get_representation(const JsError* self); -typedef struct JsError_as_native_result {union {JsNativeErrorInfo ok; }; bool is_ok;} JsError_as_native_result; -JsError_as_native_result JsError_as_native(const JsError* self); +typedef struct boa_JsError_as_native_result {union {JsNativeErrorInfo ok; }; bool is_ok;} boa_JsError_as_native_result; +boa_JsError_as_native_result boa_JsError_as_native(const JsError* self); -JsValue* JsError_as_opaque(const JsError* self); +JsValue* boa_JsError_as_opaque(const JsError* self); -void JsError_destroy(JsError* self); +void boa_JsError_destroy(JsError* self); diff --git a/ffi/boa_ffi/bindings/c/JsValue.h b/ffi/boa_ffi/bindings/c/JsValue.h index b341bf8fed0..df1963cc4cf 100644 --- a/ffi/boa_ffi/bindings/c/JsValue.h +++ b/ffi/boa_ffi/bindings/c/JsValue.h @@ -16,18 +16,18 @@ -JsValue* JsValue_from_number(double number); +JsValue* boa_JsValue_from_number(double number); -JsValue* JsValue_from_null(void); +JsValue* boa_JsValue_from_null(void); -JsValue* JsValue_from_undefined(void); +JsValue* boa_JsValue_from_undefined(void); -JsValueType JsValue_get_type(const JsValue* self); +JsValueType boa_JsValue_get_type(const JsValue* self); -typedef struct JsValue_as_number_result {union {double ok; }; bool is_ok;} JsValue_as_number_result; -JsValue_as_number_result JsValue_as_number(const JsValue* self); +typedef struct boa_JsValue_as_number_result {union {double ok; }; bool is_ok;} boa_JsValue_as_number_result; +boa_JsValue_as_number_result boa_JsValue_as_number(const JsValue* self); -void JsValue_destroy(JsValue* self); +void boa_JsValue_destroy(JsValue* self); diff --git a/ffi/boa_ffi/src/lib.rs b/ffi/boa_ffi/src/lib.rs index efcee56bc25..3a738b815b3 100644 --- a/ffi/boa_ffi/src/lib.rs +++ b/ffi/boa_ffi/src/lib.rs @@ -2,9 +2,14 @@ //! //! The C API and its ownership rules are documented in the crate's README. //! Rust applications should use `boa_engine` directly. +#![cfg_attr( + not(test), + forbid(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing) +)] /// Entry points used by the generated C bindings. #[diplomat::bridge] +#[diplomat::abi_rename = "boa_{0}"] #[allow( clippy::unnecessary_box_returns, reason = "Diplomat transfers opaque handle ownership to C through boxed pointers" @@ -20,7 +25,7 @@ pub mod ffi { /// An owned Boa context, exposed as an opaque C handle. /// /// Keep the handle on the thread that created it. Destroy it exactly once - /// with `BoaContext_destroy`; copying the pointer does not copy ownership. + /// with `boa_BoaContext_destroy`; copying the pointer does not copy ownership. /// The destructor requires a live, non-null handle. Do not call `free` on it. #[derive(Debug)] #[diplomat::opaque_mut] @@ -75,7 +80,7 @@ pub mod ffi { /// An owned JavaScript value, exposed as an opaque C handle. /// /// Keep it on its creating thread and destroy it exactly once with - /// `JsValue_destroy`. Getters borrow a live, non-null handle without + /// `boa_JsValue_destroy`. Getters borrow a live, non-null handle without /// transferring ownership. Do not call `free` on it. #[derive(Debug)] #[diplomat::opaque] @@ -161,7 +166,7 @@ pub mod ffi { /// An owned Boa error, retaining its original representation and data. /// /// Keep it on its creating thread and destroy it exactly once with - /// `JsError_destroy`. Getters borrow a live, non-null handle. + /// `boa_JsError_destroy`. Getters borrow a live, non-null handle. #[derive(Debug)] #[diplomat::opaque] pub struct JsError(BoaJsError); @@ -217,7 +222,7 @@ pub mod ffi { #[must_use] #[allow( clippy::elidable_lifetime_names, - reason = "Diplomat 0.15 requires a named lifetime for this borrowed return type" + reason = "Diplomat requires a named lifetime for this borrowed return type" )] pub fn as_native<'a>(&'a self) -> Option> { self.0.as_native().map(|error| JsNativeErrorInfo { @@ -239,7 +244,7 @@ pub mod ffi { /// Returns a new owned handle for an opaque payload, or null in C otherwise. /// /// Null and undefined payloads are present. Destroy each returned handle - /// with `JsValue_destroy`, independently of this error. No JavaScript runs. + /// with `boa_JsValue_destroy`, independently of this error. No JavaScript runs. #[must_use] pub fn as_opaque(&self) -> Option> { self.0 diff --git a/ffi/boa_ffi/tests/c/context.c b/ffi/boa_ffi/tests/c/context.c index 44aae734693..43089b1a79a 100644 --- a/ffi/boa_ffi/tests/c/context.c +++ b/ffi/boa_ffi/tests/c/context.c @@ -5,30 +5,30 @@ int main(void) { for (size_t i = 0; i < 32; ++i) { - BoaContext *first = BoaContext_new(); + BoaContext *first = boa_BoaContext_new(); if (first == NULL) { fputs("first context failed to initialize\n", stderr); return EXIT_FAILURE; } - BoaContext *second = BoaContext_new(); + BoaContext *second = boa_BoaContext_new(); if (second == NULL) { - BoaContext_destroy(first); + boa_BoaContext_destroy(first); fputs("second context failed to initialize\n", stderr); return EXIT_FAILURE; } - BoaContext_destroy(first); + boa_BoaContext_destroy(first); - BoaContext *third = BoaContext_new(); + BoaContext *third = boa_BoaContext_new(); if (third == NULL) { - BoaContext_destroy(second); + boa_BoaContext_destroy(second); fputs("third context failed to initialize\n", stderr); return EXIT_FAILURE; } - BoaContext_destroy(second); - BoaContext_destroy(third); + boa_BoaContext_destroy(second); + boa_BoaContext_destroy(third); } puts("C context lifecycle tests passed."); diff --git a/ffi/boa_ffi/tests/c/error.c b/ffi/boa_ffi/tests/c/error.c index 3fc34303a2a..259a85d09df 100644 --- a/ffi/boa_ffi/tests/c/error.c +++ b/ffi/boa_ffi/tests/c/error.c @@ -20,41 +20,41 @@ static bool native_errors(void) { for (size_t j = 0; j < sizeof(lengths) / sizeof(lengths[0]); ++j) { char message[sizeof(expected)]; memcpy(message, expected, sizeof(message)); - JsError *error = JsError_from_native(kinds[i], + JsError *error = boa_JsError_from_native(kinds[i], (DiplomatStringView){.data = message, .len = lengths[j]}); if (error == NULL) { return false; } memset(message, 'x', sizeof(message)); - JsError_as_native_result info = JsError_as_native(error); - JsValue *payload = JsError_as_opaque(error); - bool valid = JsError_get_representation(error) == JsErrorRepresentation_Native && + boa_JsError_as_native_result info = boa_JsError_as_native(error); + JsValue *payload = boa_JsError_as_opaque(error); + bool valid = boa_JsError_get_representation(error) == JsErrorRepresentation_Native && info.is_ok && info.ok.kind == kinds[i] && info.ok.message.len == lengths[j] && (lengths[j] == 0 || memcmp(info.ok.message.data, expected, lengths[j]) == 0) && payload == NULL; if (payload != NULL) { - JsValue_destroy(payload); + boa_JsValue_destroy(payload); } - JsError_destroy(error); + boa_JsError_destroy(error); if (!valid) { return false; } } } - JsError *empty = JsError_from_native(JsNativeErrorKind_Error, + JsError *empty = boa_JsError_from_native(JsNativeErrorKind_Error, (DiplomatStringView){.data = NULL, .len = 0}); if (empty == NULL) { return false; } - JsError_as_native_result empty_info = JsError_as_native(empty); + boa_JsError_as_native_result empty_info = boa_JsError_as_native(empty); bool empty_valid = empty_info.is_ok && empty_info.ok.kind == JsNativeErrorKind_Error && empty_info.ok.message.len == 0; - JsError_destroy(empty); + boa_JsError_destroy(empty); if (!empty_valid) { return false; } @@ -65,55 +65,55 @@ static bool native_errors(void) { {.data = "\xED\xA0\x80", .len = 3}, }; for (size_t i = 0; i < sizeof(rejected_messages) / sizeof(rejected_messages[0]); ++i) { - JsError *invalid = JsError_from_native(JsNativeErrorKind_Type, rejected_messages[i]); + JsError *invalid = boa_JsError_from_native(JsNativeErrorKind_Type, rejected_messages[i]); if (invalid != NULL) { - JsError_destroy(invalid); + boa_JsError_destroy(invalid); return false; } } - JsError *other = JsError_from_native(JsNativeErrorKind_Other, + JsError *other = boa_JsError_from_native(JsNativeErrorKind_Other, (DiplomatStringView){.data = "", .len = 0}); if (other != NULL) { - JsError_destroy(other); + boa_JsError_destroy(other); return false; } return true; } static bool payload_matches(JsValue *value, JsValueType kind) { - if (value == NULL || JsValue_get_type(value) != kind) { + if (value == NULL || boa_JsValue_get_type(value) != kind) { return false; } - JsValue_as_number_result number = JsValue_as_number(value); + boa_JsValue_as_number_result number = boa_JsValue_as_number(value); return kind == JsValueType_Number ? number.is_ok && number.ok == 7.0 : !number.is_ok; } static bool opaque_error(JsValue *input, JsValueType kind, bool error_first) { - JsError *error = JsError_from_opaque(input); - JsValue_destroy(input); + JsError *error = boa_JsError_from_opaque(input); + boa_JsValue_destroy(input); - bool valid = JsError_get_representation(error) == JsErrorRepresentation_Opaque && - !JsError_as_native(error).is_ok; - JsValue *first = JsError_as_opaque(error); - JsValue *second = JsError_as_opaque(error); + bool valid = boa_JsError_get_representation(error) == JsErrorRepresentation_Opaque && + !boa_JsError_as_native(error).is_ok; + JsValue *first = boa_JsError_as_opaque(error); + JsValue *second = boa_JsError_as_opaque(error); if (error_first) { - JsError_destroy(error); + boa_JsError_destroy(error); } valid = valid && payload_matches(first, kind) && payload_matches(second, kind); if (first != NULL) { - JsValue_destroy(first); + boa_JsValue_destroy(first); } valid = valid && payload_matches(second, kind); if (second != NULL) { - JsValue_destroy(second); + boa_JsValue_destroy(second); } if (!error_first) { - valid = valid && JsError_get_representation(error) == JsErrorRepresentation_Opaque; - JsError_destroy(error); + valid = valid && boa_JsError_get_representation(error) == JsErrorRepresentation_Opaque; + boa_JsError_destroy(error); } return valid; } @@ -125,9 +125,9 @@ int main(void) { } for (size_t order = 0; order < 2; ++order) { bool error_first = order == 0; - if (!opaque_error(JsValue_from_number(7.0), JsValueType_Number, error_first) || - !opaque_error(JsValue_from_null(), JsValueType_Null, error_first) || - !opaque_error(JsValue_from_undefined(), JsValueType_Undefined, error_first)) { + if (!opaque_error(boa_JsValue_from_number(7.0), JsValueType_Number, error_first) || + !opaque_error(boa_JsValue_from_null(), JsValueType_Null, error_first) || + !opaque_error(boa_JsValue_from_undefined(), JsValueType_Undefined, error_first)) { fputs("opaque error payload ownership failed\n", stderr); return EXIT_FAILURE; } diff --git a/ffi/boa_ffi/tests/c/value.c b/ffi/boa_ffi/tests/c/value.c index c6fb1ad0688..c049ecdedbc 100644 --- a/ffi/boa_ffi/tests/c/value.c +++ b/ffi/boa_ffi/tests/c/value.c @@ -26,39 +26,39 @@ int main(void) { }; for (size_t i = 0; i < sizeof(numbers) / sizeof(numbers[0]); ++i) { - JsValue *value = JsValue_from_number(numbers[i]); + JsValue *value = boa_JsValue_from_number(numbers[i]); for (size_t read = 0; read < 2; ++read) { - JsValue_as_number_result result = JsValue_as_number(value); - if (JsValue_get_type(value) != JsValueType_Number || + boa_JsValue_as_number_result result = boa_JsValue_as_number(value); + if (boa_JsValue_get_type(value) != JsValueType_Number || !result.is_ok || !same_number(result.ok, numbers[i])) { - JsValue_destroy(value); + boa_JsValue_destroy(value); fprintf(stderr, "number round trip failed at index %zu\n", i); return EXIT_FAILURE; } } - JsValue_destroy(value); + boa_JsValue_destroy(value); } - BoaContext *context = BoaContext_new(); + BoaContext *context = boa_BoaContext_new(); if (context == NULL) { fputs("context failed to initialize\n", stderr); return EXIT_FAILURE; } - JsValue *null_value = JsValue_from_null(); - JsValue *undefined_value = JsValue_from_undefined(); - JsValue *number = JsValue_from_number(42.5); - BoaContext_destroy(context); + JsValue *null_value = boa_JsValue_from_null(); + JsValue *undefined_value = boa_JsValue_from_undefined(); + JsValue *number = boa_JsValue_from_number(42.5); + boa_BoaContext_destroy(context); - JsValue_as_number_result result = JsValue_as_number(number); - bool valid = JsValue_get_type(null_value) == JsValueType_Null && - JsValue_get_type(undefined_value) == JsValueType_Undefined && - !JsValue_as_number(null_value).is_ok && - !JsValue_as_number(undefined_value).is_ok && - JsValue_get_type(number) == JsValueType_Number && + boa_JsValue_as_number_result result = boa_JsValue_as_number(number); + bool valid = boa_JsValue_get_type(null_value) == JsValueType_Null && + boa_JsValue_get_type(undefined_value) == JsValueType_Undefined && + !boa_JsValue_as_number(null_value).is_ok && + !boa_JsValue_as_number(undefined_value).is_ok && + boa_JsValue_get_type(number) == JsValueType_Number && result.is_ok && same_number(result.ok, 42.5); - JsValue_destroy(null_value); - JsValue_destroy(undefined_value); - JsValue_destroy(number); + boa_JsValue_destroy(null_value); + boa_JsValue_destroy(undefined_value); + boa_JsValue_destroy(number); if (!valid) { fputs("primitive values failed after context destruction\n", stderr); return EXIT_FAILURE;