Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/languages/rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Load this file when changing `rust/` or Rust xlang behavior.
- Do not set `FORY_PANIC_ON_ERROR=1` when running the full Rust test suite, because some tests assert on error contents.
- Avoid cosmetic filesystem or module churn when logical module names and call sites are already stable.
- Operation contexts such as `ReadContext` and `WriteContext` should sit beside the runtime facade and aggregate resolver, buffer, and config state; they are not resolver-owned submodules.
- `Fory` is the only cross-thread runtime owner. `TypeResolver`, `ReadContext`, `WriteContext`, and `RefReader` must remain `!Send + !Sync`; shared root operations deep-clone resolver state into thread-local contexts. Do not restore unsafe auto-trait implementations on those internal owners or replace their hot-path `Rc` values with `Arc` to make them shareable.
- Runtime carriers belong in `types/`, and schema or type-hash helpers belong with metadata hashing rather than generic wire/type-id modules.
- Rust derive-generated runtime paths are owned by the selected runtime crate: normal downstream crates depend on `fory`, and `fory-derive` must resolve that facade with `proc-macro-crate` and emit through `fory::__private`; direct lower-level crates may resolve `fory-core`. Do not add raw crate-path string attributes such as `#[fory(crate = "...")]`.
- `fory-core` `macro_rules!` exports, including `register_trait_type!` and helpers, must use `$crate` for runtime paths so facade re-exports stay hygienic.
Expand Down
18 changes: 0 additions & 18 deletions rust/fory-core/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,15 +359,6 @@ impl<'a> Drop for WriteContext<'a> {
}
}

// Safety: WriteContext is only shared across threads via higher-level pooling code that
// ensures single-threaded access while the context is in use. Users must never hold the same
// instance on multiple threads simultaneously; that would violate the invariants and result in
// undefined behavior. Under that assumption, marking it Send/Sync is sound.
#[allow(clippy::needless_lifetimes)]
unsafe impl<'a> Send for WriteContext<'a> {}
#[allow(clippy::needless_lifetimes)]
unsafe impl<'a> Sync for WriteContext<'a> {}

/// Deserialization state container used on a single thread at a time.
/// Sharing the same instance across threads simultaneously causes undefined behavior.
pub struct ReadContext<'a> {
Expand All @@ -391,15 +382,6 @@ pub struct ReadContext<'a> {
current_depth: u32,
}

// Safety: ReadContext follows the same invariants as WriteContext—external orchestrators ensure
// single-threaded use. Concurrent access to the same instance across threads is forbidden and
// would result in undefined behavior. With exclusive use guaranteed, the Send/Sync markers are safe
// even though Rc is used internally.
#[allow(clippy::needless_lifetimes)]
unsafe impl<'a> Send for ReadContext<'a> {}
#[allow(clippy::needless_lifetimes)]
unsafe impl<'a> Sync for ReadContext<'a> {}

impl<'a> ReadContext<'a> {
pub fn new(type_resolver: TypeResolver, config: Config) -> ReadContext<'a> {
ReadContext {
Expand Down
8 changes: 8 additions & 0 deletions rust/fory-core/src/fory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,14 @@ pub struct Fory {
config: Config,
}

// Safety: Fory is the only cross-thread owner of its resolvers. Registration requires exclusive
// access, and root operations permanently freeze the registry before the finalized resolver is
// shared. Fory never exposes the Rc values in either resolver. Each thread deep-clones the
// finalized resolver into thread-local contexts, so Rc counts and mutable context state remain
// confined to one thread.
unsafe impl Send for Fory {}
unsafe impl Sync for Fory {}

impl Default for Fory {
fn default() -> Self {
Self::builder().build()
Expand Down
4 changes: 0 additions & 4 deletions rust/fory-core/src/resolver/ref_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,6 @@ pub struct RefReader {
callbacks: Vec<UpdateCallback>,
}

// danger but useful for multi-thread
unsafe impl Send for RefReader {}
unsafe impl Sync for RefReader {}

impl RefReader {
/// Creates a new RefReader instance.
pub fn new() -> Self {
Expand Down
6 changes: 0 additions & 6 deletions rust/fory-core/src/resolver/type_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,12 +730,6 @@ pub struct TypeResolver {
xlang: bool,
}

// Safety: TypeResolver instances are only shared through higher-level synchronization that
// guarantees thread confinement for mutations, so marking them Send/Sync preserves existing
// invariants despite internal Rc usage.
unsafe impl Send for TypeResolver {}
unsafe impl Sync for TypeResolver {}

const NO_TYPE_ID: TypeId = TypeId::UNKNOWN;

impl Default for TypeResolver {
Expand Down
1 change: 1 addition & 0 deletions rust/tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ fory-derive = { path = "../fory-derive" }
fory-external-model = { path = "../api-tests/external-model" }

num-bigint = "0.4"
static_assertions = "1.1.0"

[features]
default = []
Expand Down
9 changes: 8 additions & 1 deletion rust/tests/tests/test_multi_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@
// specific language governing permissions and limitations
// under the License.

use fory_core::Fory;
use fory_core::{resolver::RefReader, Fory, ReadContext, TypeResolver, WriteContext};
use fory_derive::ForyStruct;
use static_assertions::{assert_impl_all, assert_not_impl_any};
use std::collections::HashSet;
use std::sync::{Arc, Barrier};
use std::thread;

assert_impl_all!(Fory: Send, Sync);
assert_not_impl_any!(TypeResolver: Send, Sync);
assert_not_impl_any!(ReadContext<'static>: Send, Sync);
assert_not_impl_any!(WriteContext<'static>: Send, Sync);
assert_not_impl_any!(RefReader: Send, Sync);

#[test]
fn test_simple_multi_thread() {
let fory = Arc::new(Fory::builder().xlang(false).compatible(false).build());
Expand Down
Loading