Expand description
§Apache Fory™ Rust
Apache Fory™ is a blazingly fast multi-language serialization framework powered by JIT compilation and zero-copy techniques, providing up to ultra-fast performance while maintaining ease of use and safety.
The Rust implementation provides versatile and high-performance serialization with automatic memory management and compile-time type safety.
GitHub: https://github.com/apache/fory
§Why Apache Fory™ Rust?
Apache Fory™ Rust solves the fundamental serialization dilemma: you shouldn’t have to choose between performance and developer experience. Traditional frameworks force you to pick between fast but fragile binary formats, flexible but slow text-based protocols, or complex solutions that don’t support your language’s advanced features.
Key differentiators:
- Fast: Zero-copy deserialization and optimized binary protocols
- Cross-Language: Seamlessly serialize/deserialize data across Java, Python, C++, Go, JavaScript, and Rust
- Type-Safe: Compile-time type checking with derive macros
- Circular References: Automatic tracking of shared and circular references with
Rc/Arcand weak pointers - Polymorphic: Serialize trait objects with
Box<dyn Trait>,Rc<dyn Trait>, andArc<dyn Trait> - Schema Evolution: Compatible mode for independent schema changes
- Two Formats: Object graph serialization and zero-copy row-based format
§Quick Start
Add Apache Fory™ to your Cargo.toml:
[dependencies]
fory = "0.13"
fory-derive = "0.13"§Basic Example
use fory::{Fory, Error, Reader};
use fory::{ForyEnum, ForyStruct, ForyUnion};
#[derive(ForyStruct, Debug, PartialEq)]
struct User {
name: String,
age: i32,
email: String,
}
let mut fory = Fory::builder().xlang(true).build();
fory.register_by_name::<User>("example", "User")?;
let user = User {
name: "Alice".to_string(),
age: 30,
email: "alice@example.com".to_string(),
};
// Serialize and deserialize
let bytes = fory.serialize(&user)?;
let decoded: User = fory.deserialize(&bytes)?;
assert_eq!(user, decoded);
// Serialize to specified buffer and deserialize from it
let mut buf: Vec<u8> = vec![];
fory.serialize_to(&mut buf, &user)?;
let mut reader = Reader::new(&buf);
let decoded: User = fory.deserialize_from(&mut reader)?;
assert_eq!(user, decoded);§Core Features
Apache Fory™ Rust provides seven major feature categories, each designed to solve specific serialization challenges in modern applications.
§1. Object Graph Serialization
What it does: Automatically serializes complex nested data structures while preserving relationships and hierarchies.
Why it matters: Most real-world applications deal with complex domain models, not just flat data structures. Apache Fory™ handles arbitrary nesting depth, collections, and optional fields without manual mapping code.
Technical approach: The #[derive(ForyStruct)] macro generates efficient
serialization code at compile time using procedural macros. This eliminates runtime
reflection overhead while maintaining type safety.
Key capabilities:
- Nested struct serialization with arbitrary depth
- Collection types (
Vec,HashMap,HashSet,BTreeMap) - Optional fields with
Option<T> - Automatic handling of primitive types and strings
- Efficient binary encoding with variable-length integers
use fory::{Fory, Error};
use fory::{ForyEnum, ForyStruct, ForyUnion};
use std::collections::HashMap;
#[derive(ForyStruct, Debug, PartialEq)]
struct Person {
name: String,
age: i32,
address: Address,
hobbies: Vec<String>,
metadata: HashMap<String, String>,
}
#[derive(ForyStruct, Debug, PartialEq)]
struct Address {
street: String,
city: String,
country: String,
}
let mut fory = Fory::builder().xlang(true).build();
fory.register_by_name::<Address>("example", "Address")?;
fory.register_by_name::<Person>("example", "Person")?;
let person = Person {
name: "John Doe".to_string(),
age: 30,
address: Address {
street: "123 Main St".to_string(),
city: "New York".to_string(),
country: "USA".to_string(),
},
hobbies: vec!["reading".to_string(), "coding".to_string()],
metadata: HashMap::from([
("role".to_string(), "developer".to_string()),
]),
};
let bytes = fory.serialize(&person)?;
let decoded: Person = fory.deserialize(&bytes)?;
assert_eq!(person, decoded);§2. Native-Mode Shared and Circular References
What it does: Automatically tracks and preserves reference identity for shared
objects using Rc<T> and Arc<T>, and handles circular references using weak pointers.
The examples in this section use native mode because Rc, Arc, and weak-pointer
identity are Rust object-graph features.
Why it matters: Graph-like data structures (trees, linked lists, object-relational models) are common in real applications but notoriously difficult to serialize. Most frameworks either panic on circular references or require extensive manual handling.
Technical approach: Apache Fory™ maintains a reference tracking map during
serialization. When the same object is encountered multiple times, it’s serialized
only once and subsequent references use IDs. Weak pointers (RcWeak<T>, ArcWeak<T>)
break cycles by serializing as references without strong ownership.
Benefits:
- Space efficiency: No data duplication in serialized output
- Reference identity preservation: Deserialized objects maintain the same sharing relationships
- Circular reference support: Use
RcWeak<T>andArcWeak<T>to break cycles - Forward reference resolution: Callbacks handle weak pointers appearing before targets
§Shared References with Rc/Arc
use fory::Fory;
use fory::Error;
use std::rc::Rc;
let fory = Fory::builder().xlang(false).build();
let shared = Rc::new(String::from("shared_value"));
let data = vec![shared.clone(), shared.clone(), shared.clone()];
let bytes = fory.serialize(&data)?;
let decoded: Vec<Rc<String>> = fory.deserialize(&bytes)?;
assert_eq!(decoded.len(), 3);
assert!(Rc::ptr_eq(&decoded[0], &decoded[1]));
assert!(Rc::ptr_eq(&decoded[1], &decoded[2]));For thread-safe shared references, use Arc<T>:
use fory::Fory;
use fory::Error;
use std::sync::Arc;
let fory = Fory::builder().xlang(false).build();
let shared = Arc::new(String::from("shared_value"));
let data = vec![shared.clone(), shared.clone()];
let bytes = fory.serialize(&data)?;
let decoded: Vec<Arc<String>> = fory.deserialize(&bytes)?;
assert!(Arc::ptr_eq(&decoded[0], &decoded[1]));§Circular References with Weak Pointers
How it works:
- Weak pointers serialize as references to their target objects
- If the strong pointer has been dropped, weak serializes as
Null - Forward references (weak appearing before target) are resolved via callbacks
- All clones of a weak pointer share the same internal cell for automatic updates
use fory::{Fory, Error, RcWeak};
use fory::{ForyEnum, ForyStruct, ForyUnion};
use std::rc::Rc;
use std::cell::RefCell;
#[derive(ForyStruct, Debug)]
struct Node {
value: i32,
parent: RcWeak<RefCell<Node>>,
children: Vec<Rc<RefCell<Node>>>,
}
let mut fory = Fory::builder().xlang(false).track_ref(true).build();
fory.register::<Node>(2000)?;
let parent = Rc::new(RefCell::new(Node {
value: 1,
parent: RcWeak::new(),
children: vec![],
}));
let child1 = Rc::new(RefCell::new(Node {
value: 2,
parent: RcWeak::from(&parent),
children: vec![],
}));
parent.borrow_mut().children.push(child1.clone());
let bytes = fory.serialize(&parent)?;
let decoded: Rc<RefCell<Node>> = fory.deserialize(&bytes)?;
assert_eq!(decoded.borrow().children.len(), 1);
let upgraded_parent = decoded.borrow().children[0].borrow().parent.upgrade().unwrap();
assert!(Rc::ptr_eq(&decoded, &upgraded_parent));Thread-Safe Circular Graphs with Arc:
use fory::{Fory, Error, ArcWeak};
use fory::{ForyEnum, ForyStruct, ForyUnion};
use std::sync::{Arc, Mutex};
#[derive(ForyStruct)]
struct Node {
val: i32,
parent: ArcWeak<Mutex<Node>>,
children: Vec<Arc<Mutex<Node>>>,
}
let mut fory = Fory::builder().xlang(false).track_ref(true).build();
fory.register::<Node>(6000)?;
let parent = Arc::new(Mutex::new(Node {
val: 10,
parent: ArcWeak::new(),
children: vec![],
}));
let child = Arc::new(Mutex::new(Node {
val: 20,
parent: ArcWeak::from(&parent),
children: vec![],
}));
parent.lock().unwrap().children.push(child.clone());
let bytes = fory.serialize(&parent)?;
let decoded: Arc<Mutex<Node>> = fory.deserialize(&bytes)?;
assert_eq!(decoded.lock().unwrap().children.len(), 1);§3. Native-Mode Trait Object Serialization
What it does: Enables polymorphic serialization through trait objects, supporting
dynamic dispatch and type flexibility.
The examples in this section use native mode because Rust trait objects and dyn Any
dispatch are Rust runtime features.
Why it matters: Rust’s trait system is powerful for abstraction, but serializing
Box<dyn Trait> is notoriously difficult. This feature is essential for plugin systems,
heterogeneous collections, and extensible architectures.
Technical approach: The register_trait_type! macro generates type registration
and dispatch code for trait implementations. During serialization, type IDs are written
alongside data, enabling correct deserialization to the concrete type.
Supported trait object types:
Box<dyn Trait>- Owned trait objectsRc<dyn Trait>- Reference-counted trait objectsArc<dyn Trait>- Thread-safe reference-counted trait objectsRc<dyn Any>/Arc<dyn Any>- Runtime type dispatch without custom traits- Collections:
Vec<Box<dyn Trait>>,HashMap<K, Box<dyn Trait>>
§Basic Trait Object Serialization
use fory::{Fory, register_trait_type, Serializer, Error};
use fory::{ForyEnum, ForyStruct, ForyUnion};
trait Animal: Serializer {
fn speak(&self) -> String;
fn name(&self) -> &str;
}
#[derive(ForyStruct, Debug)]
struct Dog { name: String, breed: String }
impl Animal for Dog {
fn speak(&self) -> String { "Woof!".to_string() }
fn name(&self) -> &str { &self.name }
}
#[derive(ForyStruct, Debug)]
struct Cat { name: String, color: String }
impl Animal for Cat {
fn speak(&self) -> String { "Meow!".to_string() }
fn name(&self) -> &str { &self.name }
}
register_trait_type!(Animal, Dog, Cat);
#[derive(ForyStruct)]
struct Zoo {
star_animal: Box<dyn Animal>,
}
let mut fory = Fory::builder().xlang(false).compatible(true).build();
fory.register::<Dog>(100)?;
fory.register::<Cat>(101)?;
fory.register::<Zoo>(102)?;
let zoo = Zoo {
star_animal: Box::new(Dog {
name: "Buddy".to_string(),
breed: "Labrador".to_string(),
}),
};
let bytes = fory.serialize(&zoo)?;
let decoded: Zoo = fory.deserialize(&bytes)?;
assert_eq!(decoded.star_animal.name(), "Buddy");
assert_eq!(decoded.star_animal.speak(), "Woof!");§Serializing dyn Any Trait Objects
What it does: Supports serializing Rc<dyn Any> and Arc<dyn Any> for maximum
runtime type flexibility without defining custom traits.
When to use: Plugin systems, dynamic type handling, or when you need runtime type dispatch without compile-time trait definitions.
Key points:
- Works with any type that implements
Serializer - Requires downcasting after deserialization to access the concrete type
- Type information is preserved during serialization
use fory::Fory;
use fory::Error;
use std::rc::Rc;
use std::any::Any;
use fory::{ForyEnum, ForyStruct, ForyUnion};
#[derive(ForyStruct)]
struct Dog { name: String }
let mut fory = Fory::builder().xlang(false).build();
fory.register::<Dog>(100)?;
let dog: Rc<dyn Any> = Rc::new(Dog {
name: "Rex".to_string()
});
let bytes = fory.serialize(&dog)?;
let decoded: Rc<dyn Any> = fory.deserialize(&bytes)?;
let unwrapped = decoded.downcast_ref::<Dog>().unwrap();
assert_eq!(unwrapped.name, "Rex");For thread-safe scenarios, use Arc<dyn Any>:
use fory::Fory;
use fory::Error;
use std::sync::Arc;
use std::any::Any;
use fory::{ForyEnum, ForyStruct, ForyUnion};
#[derive(ForyStruct)]
struct Cat { name: String }
let mut fory = Fory::builder().xlang(false).build();
fory.register::<Cat>(101)?;
let cat: Arc<dyn Any> = Arc::new(Cat {
name: "Whiskers".to_string()
});
let bytes = fory.serialize(&cat)?;
let decoded: Arc<dyn Any> = fory.deserialize(&bytes)?;
let unwrapped = decoded.downcast_ref::<Cat>().unwrap();
assert_eq!(unwrapped.name, "Whiskers");§Rc/Arc-Based Trait Objects in Structs
For struct fields containing Rc<dyn Trait> or Arc<dyn Trait>, Apache Fory™
automatically handles the conversion without needing wrappers:
use fory::{Fory, register_trait_type, Serializer, Error};
use fory::{ForyEnum, ForyStruct, ForyUnion};
use std::sync::Arc;
use std::rc::Rc;
trait Animal: Serializer {
fn name(&self) -> &str;
}
#[derive(ForyStruct, Debug)]
struct Dog { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
}
#[derive(ForyStruct, Debug)]
struct Cat { name: String }
impl Animal for Cat {
fn name(&self) -> &str { &self.name }
}
register_trait_type!(Animal, Dog, Cat);
#[derive(ForyStruct)]
struct AnimalShelter {
animals_rc: Vec<Rc<dyn Animal>>,
animals_arc: Vec<Arc<dyn Animal>>,
}
let mut fory = Fory::builder().xlang(false).compatible(true).build();
fory.register::<Dog>(100)?;
fory.register::<Cat>(101)?;
fory.register::<AnimalShelter>(102)?;
let shelter = AnimalShelter {
animals_rc: vec![
Rc::new(Dog { name: "Rex".to_string() }),
Rc::new(Cat { name: "Mittens".to_string() }),
],
animals_arc: vec![
Arc::new(Dog { name: "Buddy".to_string() }),
],
};
let bytes = fory.serialize(&shelter)?;
let decoded: AnimalShelter = fory.deserialize(&bytes)?;
assert_eq!(decoded.animals_rc[0].name(), "Rex");
assert_eq!(decoded.animals_arc[0].name(), "Buddy");§Standalone Trait Object Serialization with Wrappers
Due to Rust’s orphan rule, Rc<dyn Trait> and Arc<dyn Trait> cannot implement
Serializer directly. For standalone serialization (not inside struct fields),
the register_trait_type! macro generates wrapper types.
Note: If you don’t want to use wrapper types, you can serialize as Rc<dyn Any>
or Arc<dyn Any> instead (see the dyn Any section above).
The register_trait_type! macro generates AnimalRc and AnimalArc wrapper types:
use fory::{Fory, Error, register_trait_type, Serializer};
use fory::{ForyEnum, ForyStruct, ForyUnion};
use std::sync::Arc;
use std::rc::Rc;
trait Animal: Serializer {
fn name(&self) -> &str;
}
#[derive(ForyStruct, Debug)]
struct Dog { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
}
register_trait_type!(Animal, Dog);
let mut fory = Fory::builder().xlang(false).compatible(true).build();
fory.register::<Dog>(100)?;
// For Rc<dyn Trait>
let dog_rc: Rc<dyn Animal> = Rc::new(Dog { name: "Rex".to_string() });
let wrapper = AnimalRc::from(dog_rc);
let bytes = fory.serialize(&wrapper)?;
let decoded: AnimalRc = fory.deserialize(&bytes)?;
// Unwrap back to Rc<dyn Animal>
let unwrapped: Rc<dyn Animal> = decoded.unwrap();
assert_eq!(unwrapped.name(), "Rex");
// For Arc<dyn Trait>
let dog_arc: Arc<dyn Animal> = Arc::new(Dog { name: "Buddy".to_string() });
let wrapper = AnimalArc::from(dog_arc);
let bytes = fory.serialize(&wrapper)?;
let decoded: AnimalArc = fory.deserialize(&bytes)?;
let unwrapped: Arc<dyn Animal> = decoded.unwrap();
assert_eq!(unwrapped.name(), "Buddy");§4. Schema Evolution
What it does: Supports schema evolution in Compatible mode, allowing serialization and deserialization peers to have different type definitions.
Why it matters: In distributed systems and microservices, different services evolve independently. Schema evolution enables zero-downtime deployments where services can be updated gradually without breaking communication.
Technical approach: In Compatible mode, Apache Fory™ includes field names and type metadata in the serialized data. During deserialization, fields are matched by name, allowing for additions, deletions, and reordering.
Features:
- Add new fields with default values
- Remove obsolete fields (skipped during deserialization)
- Change field nullability (
T↔Option<T>) - Reorder fields (matched by name, not position)
- Type-safe fallback to default values for missing fields
Compatibility rules:
- Field names must match (case-sensitive)
- Type changes are not supported (except nullable/non-nullable)
- Nested struct types must be registered on both sides
use fory::{Fory, Error};
use fory::{ForyEnum, ForyStruct, ForyUnion};
use std::collections::HashMap;
#[derive(ForyStruct, Debug)]
struct PersonV1 {
name: String,
age: i32,
address: String,
}
#[derive(ForyStruct, Debug)]
struct PersonV2 {
name: String,
age: i32,
phone: Option<String>,
metadata: HashMap<String, String>,
}
let mut fory1 = Fory::builder().xlang(true).compatible(true).build();
fory1.register_by_name::<PersonV1>("example", "Person")?;
let mut fory2 = Fory::builder().xlang(true).compatible(true).build();
fory2.register_by_name::<PersonV2>("example", "Person")?;
let person_v1 = PersonV1 {
name: "Alice".to_string(),
age: 30,
address: "123 Main St".to_string(),
};
let bytes = fory1.serialize(&person_v1)?;
let person_v2: PersonV2 = fory2.deserialize(&bytes)?;
assert_eq!(person_v2.name, "Alice");
assert_eq!(person_v2.age, 30);
assert_eq!(person_v2.phone, None);§5. Native-Mode Enum Support
What it does: Comprehensive enum support with three variant types (unit, unnamed, named) and full schema evolution in Compatible mode.
Why it matters: Enums are essential for state machines, status codes, type discriminators, and domain modeling. Supporting all variant types with schema evolution enables flexible API evolution without breaking compatibility.
Technical approach: Each variant is assigned an ordinal value (0, 1, 2, …). In compatible mode, variants are encoded with both a tag (ordinal) and a type marker (2 bits: 0b0=Unit, 0b1=Unnamed, 0b10=Named). Named variants generate meta types for field-level evolution.
Variant Types:
- Unit: C-style enums (
Status::Active) - Unnamed: Tuple-like variants (
Message::Pair(String, i32)) - Named: Struct-like variants (
Event::Click { x: i32, y: i32 })
Features:
- Efficient varint encoding for variant ordinals
- Schema evolution support (add/remove variants, add/remove fields)
- Default variant support with
#[default] - Automatic type mismatch handling
use fory::Fory;
use fory::Error;
use fory::ForyUnion;
#[derive(Default, ForyUnion, Debug, PartialEq)]
enum Value {
#[default]
Null,
Bool(bool),
Number(f64),
Text(String),
Object { name: String, value: i32 },
}
let mut fory = Fory::builder().xlang(false).build();
fory.register::<Value>(1)?;
let value = Value::Object { name: "score".to_string(), value: 100 };
let bytes = fory.serialize(&value)?;
let decoded: Value = fory.deserialize(&bytes)?;
assert_eq!(value, decoded);Schema Evolution:
Compatible mode enables robust schema evolution with variant type encoding:
use fory::Fory;
use fory::Error;
use fory::ForyUnion;
// Old version with 2 fields
#[derive(ForyUnion, Debug)]
enum OldEvent {
Click { x: i32, y: i32 },
}
// New version with 3 fields - added timestamp
#[derive(ForyUnion, Debug)]
enum NewEvent {
Click { x: i32, y: i32, timestamp: u64 },
}
let mut fory_old = Fory::builder().xlang(false).compatible(true).build();
fory_old.register::<OldEvent>(5)?;
let mut fory_new = Fory::builder().xlang(false).compatible(true).build();
fory_new.register::<NewEvent>(5)?;
// Serialize with old schema (2 fields)
let old_bytes = fory_old.serialize(&OldEvent::Click { x: 100, y: 200 })?;
// Deserialize with new schema (3 fields) - timestamp gets default value (0)
let new_event: NewEvent = fory_new.deserialize(&old_bytes)?;
match new_event {
NewEvent::Click { x, y, timestamp } => {
assert_eq!(x, 100);
assert_eq!(y, 200);
assert_eq!(timestamp, 0); // Default value for missing field
}
}Evolution capabilities:
- Unknown variants fall back to default variant
- Named variant fields: add/remove fields (missing fields use defaults)
- Unnamed variant elements: add/remove elements (extras skipped, missing use defaults)
- Variant type mismatches automatically use default value of current variant
§6. Native-Mode Tuple Support
What it does: Supports tuples up to 22 elements with automatic heterogeneous type handling and schema evolution in compatible mode.
Why it matters: Tuples provide lightweight aggregation without defining full structs, useful for temporary groupings, function return values, and ad-hoc data structures.
Technical approach: Each tuple size (1-22) has a specialized Serializer implementation.
In schema-consistent mode, elements are serialized sequentially without overhead. In compatible
mode, the tuple is serialized as a heterogeneous collection with type metadata for each element.
Features:
- Automatic serialization for tuples from 1 to 22 elements
- Heterogeneous type support (each element can be a different type)
- Schema evolution in Compatible mode (handles missing/extra elements)
- Default values for missing elements during deserialization
use fory::Fory;
use fory::Error;
let mut fory = Fory::builder().xlang(false).build();
// Tuple with heterogeneous types
let data: (i32, String, bool, Vec<i32>) = (
42,
"hello".to_string(),
true,
vec![1, 2, 3],
);
let bytes = fory.serialize(&data)?;
let decoded: (i32, String, bool, Vec<i32>) = fory.deserialize(&bytes)?;
assert_eq!(data, decoded);§7. Native-Mode Custom Serializers
What it does: Allows manual implementation of the Serializer trait for types
that don’t support #[derive(ForyStruct)].
When to use:
- External types from other crates that you can’t modify
- Types with special serialization requirements
- Existing external binary format interoperability
- Performance-critical custom encoding
- Complex types that require special handling
Technical approach: Implement the Serializer trait’s fory_write_data() and
fory_read_data() methods to control exactly how data is written to and read from
the binary buffer.
use fory::{Fory, TypeResolver, ReadContext, WriteContext, Serializer, ForyDefault, Error};
use std::any::Any;
#[derive(Debug, PartialEq, Default)]
struct CustomType {
value: i32,
name: String,
}
impl Serializer for CustomType {
fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error> {
context.writer.write_i32(self.value);
context.writer.write_var_u32(self.name.len() as u32);
context.writer.write_utf8_string(&self.name);
Ok(())
}
fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
let value = context.reader.read_i32()?;
let len = context.reader.read_var_u32()? as usize;
let name = context.reader.read_utf8_string(len)?;
Ok(Self { value, name })
}
fn fory_type_id_dyn(&self, type_resolver: &TypeResolver) -> Result<fory::TypeId, Error> {
Self::fory_get_type_id(type_resolver)
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl ForyDefault for CustomType {
fn fory_default() -> Self {
Self::default()
}
}
let mut fory = Fory::builder().xlang(false).build();
fory.register_serializer::<CustomType>(100)?;
let custom = CustomType {
value: 42,
name: "test".to_string(),
};
let bytes = fory.serialize(&custom)?;
let decoded: CustomType = fory.deserialize(&bytes)?;
assert_eq!(custom, decoded);§7. Row-Based Serialization
What it does: Provides a high-performance row format for zero-copy deserialization, enabling random access to fields directly from binary data without full object reconstruction.
Why it matters: Traditional serialization reconstructs entire objects in memory. For analytics workloads or when you only need a few fields from large objects, this is wasteful. Row format provides O(1) field access without deserialization.
Technical approach: Fields are encoded in a binary row with fixed offsets for primitives. Variable-length data (strings, collections) are stored with offset pointers. A null bitmap tracks which fields are present. The generated code provides accessor methods that read directly from the binary buffer.
Key benefits:
- Zero-copy access: Read fields without allocating or copying data
- Partial deserialization: Access only the fields you need
- Memory-mapped files: Work with data larger than RAM
- Cache-friendly: Sequential memory layout for better CPU cache utilization
- Lazy evaluation: Defer expensive operations until field access
When to use row format:
- Analytics workloads with selective field access
- Large datasets where only a subset of fields is needed
- Memory-constrained environments
- High-throughput data pipelines
- Reading from memory-mapped files or shared memory
Performance characteristics:
| Operation | Object Format | Row Format |
|---|---|---|
| Full deserialization | Allocates all objects | Zero allocation |
| Single field access | Full deserialization required | Direct offset read (O(1)) |
| Memory usage | Full object graph in memory | Only accessed fields in memory |
| Suitable for | Small objects, full access | Large objects, selective access |
use fory::{to_row, from_row};
use fory_derive::ForyRow;
use std::collections::BTreeMap;
#[derive(ForyRow)]
struct UserProfile {
id: i64,
username: String,
email: String,
scores: Vec<i32>,
preferences: BTreeMap<String, String>,
is_active: bool,
}
let profile = UserProfile {
id: 12345,
username: "alice".to_string(),
email: "alice@example.com".to_string(),
scores: vec![95, 87, 92, 88],
preferences: BTreeMap::from([
("theme".to_string(), "dark".to_string()),
("language".to_string(), "en".to_string()),
]),
is_active: true,
};
let row_data = to_row(&profile).unwrap();
let row = from_row::<UserProfile>(&row_data);
assert_eq!(row.id(), 12345);
assert_eq!(row.username(), "alice");
assert_eq!(row.is_active(), true);
let scores = row.scores();
assert_eq!(scores.size(), 4);
assert_eq!(scores.get(0).unwrap(), 95);§Supported Types
Apache Fory™ supports a comprehensive type system for maximum flexibility.
§Primitive Types
bool- Boolean valuesi8,i16,i32,i64- Signed integersf32,f64- Floating point numbersString- UTF-8 encoded strings
§Collections
Vec<T>- Dynamic arraysHashMap<K, V>- Hash-based mapsBTreeMap<K, V>- Ordered mapsHashSet<T>- Hash-based setsOption<T>- Optional values
§Smart Pointers
Box<T>- Heap allocationRc<T>- Reference counting (shared references tracked automatically)Arc<T>- Thread-safe reference counting (shared references tracked)RcWeak<T>- Weak reference toRc<T>(breaks circular references)ArcWeak<T>- Weak reference toArc<T>(breaks circular references)RefCell<T>- Interior mutability with runtime borrow checkingMutex<T>- Thread-safe interior mutability
§Date and Time
Date- Date without timezone, with epoch-day accessors and checked day arithmeticTimestamp- Point in time, with epoch unit conversions and checked duration arithmeticDuration- Signed duration, with normalized parts, total unit conversions, and checked arithmeticchrono::NaiveDate,chrono::NaiveDateTime, andchrono::Durationwhen thechronofeature is enabled
§Custom Types
- Structs with
#[derive(ForyStruct)]- Object graph serialization - Structs with
#[derive(ForyRow)]- Row-based serialization - C-style enums with
#[derive(ForyStruct)]- Enum support - Manual
Serializerimplementation - Custom serialization logic
§Trait Objects
Box<dyn Trait>- Owned trait objectsRc<dyn Trait>- Reference-counted trait objectsArc<dyn Trait>- Thread-safe reference-counted trait objectsRc<dyn Any>- Runtime type dispatch without custom traitsArc<dyn Any>- Thread-safe runtime type dispatch
§Wire Modes And Schema Evolution
Apache Fory™ Rust supports two wire modes:
- Xlang mode is selected with
.xlang(true). It is the default wire mode and is used for cross-language payloads. Whencompatibleis omitted, xlang mode uses compatible schema evolution so independently deployed peers can add, remove, or reorder fields. - Native mode is selected with
.xlang(false). Use it for Rust-only payloads. Whencompatibleis omitted, native mode uses schema-consistent payloads for the smaller same-schema format.
use fory::Fory;
// Xlang mode with compatible schema evolution.
let xlang = Fory::builder().xlang(true).build();
// Native mode with schema-consistent payloads.
let native = Fory::builder().xlang(false).build();
// Native mode with compatible schema evolution.
let native_compatible = Fory::builder().xlang(false).compatible(true).build();§Cross-Language Serialization
What it enables: Seamless data exchange across Java, Python, C++, Go, JavaScript, and Rust implementations.
Why it matters: Microservices architectures often use multiple languages. Apache Fory™ provides a common binary protocol without IDL files or code generation.
How to enable:
use fory::Fory;
use fory::{ForyEnum, ForyStruct, ForyUnion};
let mut fory = Fory::builder().xlang(true).build();
#[derive(ForyStruct)]
struct MyStruct {
field1: i32,
field2: String,
}
fory.register_by_name::<MyStruct>("com.example", "MyStruct").unwrap();Type registration strategies:
- ID-based registration:
fory.register::<T>(id)- Fastest, requires coordination - Name-based registration:
fory.register_by_name::<T>(namespace, name)- Automatic cross-language mapping
§Performance Characteristics
Apache Fory™ Rust is designed for maximum performance through multiple techniques:
Compile-time code generation:
- Procedural macros generate specialized serialization code
- Zero runtime overhead, no reflection
- Monomorphization for type-specific optimizations
Zero-copy techniques:
- Row format enables direct memory access
- No intermediate object allocation
- Memory-mapped file support
Space efficiency:
- Variable-length integer encoding
- Reference deduplication (shared objects serialized once)
- Compact binary format
Buffer management:
- Pre-allocation based on
fory_reserved_space()hints - Minimized reallocations
- Little-endian layout for modern CPUs
§Error Handling
Apache Fory™ uses Result<T, Error> for all fallible operations, providing
comprehensive error handling:
use fory::{Fory, Error};
use fory::{ForyEnum, ForyStruct, ForyUnion};
#[derive(ForyStruct)]
struct Data {
value: i32,
}
fn process_data(bytes: &[u8]) -> Result<Data, Error> {
let mut fory = Fory::builder().xlang(true).build();
fory.register_by_name::<Data>("example", "Data")?;
let data: Data = fory.deserialize(bytes)?;
Ok(data)
}§Thread Safety
Fory implements Send and Sync, so a single instance can be shared across threads
(for example via Arc<Fory>) for concurrent serialization and deserialization. The
internal context pools grow lazily and rely on thread-safe primitives, allowing multiple
workers to reuse buffers without additional coordination.
use std::sync::Arc;
use std::thread;
use fory::Fory;
use fory::{ForyEnum, ForyStruct, ForyUnion};
#[derive(ForyStruct, Clone, Copy, Debug)]
struct Item {
value: i32,
}
let mut fory = Fory::builder().xlang(true).build();
fory.register_by_name::<Item>("example", "Item").unwrap();
let fory = Arc::new(fory);
let handles: Vec<_> = (0..8)
.map(|i| {
let shared = Arc::clone(&fory);
thread::spawn(move || {
let item = Item { value: i };
shared.serialize(&item).unwrap()
})
})
.collect();
for handle in handles {
let bytes = handle.join().unwrap();
let item: Item = fory.deserialize(&bytes).unwrap();
assert!(item.value >= 0);
}Best practice: Perform type registration (e.g., fory.register::<T>(id)) before
spawning worker threads so metadata is ready, then share the configured instance.
§Examples
See the tests/ directory for comprehensive examples:
tests/tests/test_complex_struct.rs- Complex nested structurestests/tests/test_rc_arc_trait_object.rs- Trait object serializationtests/tests/test_weak.rs- Circular reference handlingtests/tests/test_cross_language.rs- Cross-language compatibility
§Troubleshooting
- Type registry errors: Errors such as
TypeId ... not found in type_info registrymean the type was never registered with the activeForyinstance. Ensure every serializable struct, enum, or trait implementation callsregister::<T>(type_id)before use, and reuse the same IDs when deserializing. - Quick error lookup: Always prefer the static constructors on
fory_core::error::Error—for exampleError::type_mismatch,Error::invalid_data, orError::unknown. They keep diagnostics consistent and allow optional panic-on-error debugging. - Panic on error for backtraces: Set
FORY_PANIC_ON_ERROR=1(ortrue) together withRUST_BACKTRACE=1while running tests or binaries to panic exactly where an error is constructed. Unset the variable afterwards so production paths keep returningResult. - Struct field tracing: Add the
#[fory(debug)]attribute (or#[fory(debug = true)]) next to#[derive(ForyStruct)]when you need per-field instrumentation. Once compiled with debug hooks, callset_before_write_field_func,set_after_write_field_func,set_before_read_field_func, orset_after_read_field_funcfromfory_core::serializer::struct_to install custom callbacks, and usereset_struct_debug_hooks()to restore defaults. - Lightweight logging: If custom callbacks are unnecessary, enable
ENABLE_FORY_DEBUG_OUTPUT=1to have the default hook handlers print field-level read/write events. This is useful for spotting cursor misalignment or unexpected buffer growth. - Test-time hygiene: Some integration tests expect
FORY_PANIC_ON_ERRORto stay unset. Export it only during focused debugging, and rely on targeted commands such ascargo test --features tests -p tests --test <case>when isolating failures.
§Documentation
- Protocol Specification - Binary protocol details
- Row Format Specification - Row format internals
- Type Mapping - Cross-language type mappings
- API Documentation - Complete API reference
- GitHub Repository - Source code and issue tracking
Macros§
- register_
trait_ type - Macro to register trait object conversions for custom traits.
Structs§
- ArcWeak
- A serializable runtime wrapper around
std::sync::Weak<T>. - BFloat16
- Date
- Date without timezone, represented as signed days since Unix epoch.
- Decimal
- Duration
- Signed duration, represented as seconds and normalized nanoseconds.
- Float16
- IEEE 754 binary16 (half-precision) floating-point type.
- Fory
- The main Fory serialization framework instance.
- Fory
Builder - Builder for configuring a
Foryruntime before first use. - RcWeak
- A serializable runtime wrapper around
std::rc::Weak<T>. - Read
Context - Deserialization state container used on a single thread at a time. Sharing the same instance across threads simultaneously causes undefined behavior.
- Reader
- Timestamp
- Point in time, represented as seconds and nanoseconds since Unix epoch.
- Type
Resolver - TypeResolver is a resolver for fast type/serializer dispatch.
- Write
Context - Serialization state container used on a single thread at a time. Sharing the same instance across threads simultaneously causes undefined behavior.
- Writer
Enums§
- Error
- Error type for Fory serialization and deserialization operations.
- RefFlag
- RefMode
- Controls how reference and null flags are handled during serialization.
- TypeId
Traits§
- Fory
Default - Trait for creating default values during Fory deserialization.
- Serializer
- Core trait for Fory serialization and deserialization.
Functions§
Derive Macros§
- Fory
Enum - Derive macro for pure enum serialization.
- ForyRow
- Derive macro for row-based serialization.
- Fory
Struct - Derive macro for struct serialization.
- Fory
Union - Derive macro for tagged union serialization.