diff --git a/docs/guide/java/row-format.md b/docs/guide/java/row-format.md index 477f9ec136..471bfb2da9 100644 --- a/docs/guide/java/row-format.md +++ b/docs/guide/java/row-format.md @@ -28,7 +28,7 @@ Row format is a cache-friendly binary random access format that supports: - **Zero-copy access**: Read fields directly from binary without allocating objects - **Partial deserialization**: Access only the fields you need - **Skipping serialization**: Skip serialization of fields you don't need -- **Cross-language compatibility**: Works across Python, Java, C++, and other languages +- **Cross-language compatibility**: Standard rows work across Python, Java, C++, and Rust - **Column format conversion**: Can convert to Apache Arrow columnar format automatically ## Basic Usage @@ -62,7 +62,7 @@ for (int i = 0; i < 1_000_000; i++) { } foo.f4 = bars; -// Encode to row format (cross-language compatible with Python/C++) +// Encode to row format (cross-language compatible with Python/C++/Rust) BinaryRow binaryRow = encoder.toRow(foo); // Zero-copy random access without full deserialization @@ -95,13 +95,13 @@ straight to the ordinal row getter without another schema map lookup or typed ha ## Key Benefits -| Feature | Description | -| ----------------------- | ------------------------------------------------------ | -| Zero-Copy Access | Read nested fields without deserializing entire object | -| Memory Efficiency | Memory-map large datasets directly from disk | -| Cross-Language | Binary format compatible between Java, Python, C++ | -| Partial Deserialization | Deserialize only specific elements you need | -| High Performance | Skip unnecessary data parsing for analytics workloads | +| Feature | Description | +| ----------------------- | -------------------------------------------------------- | +| Zero-Copy Access | Read nested fields without deserializing entire object | +| Memory Efficiency | Memory-map large datasets directly from disk | +| Cross-Language | Binary format compatible between Java, Python, C++, Rust | +| Partial Deserialization | Deserialize only specific elements you need | +| High Performance | Skip unnecessary data parsing for analytics workloads | ## When to Use Row Format @@ -121,20 +121,19 @@ Row format works seamlessly across languages. The same binary data can be access ```python import pyfory -import pyarrow as pa from dataclasses import dataclass from typing import List, Dict @dataclass class Bar: f1: str - f2: List[pa.int64] + f2: List[pyfory.Int64] @dataclass class Foo: - f1: pa.int32 - f2: List[pa.int32] - f3: Dict[str, pa.int32] + f1: pyfory.Int32 + f2: List[pyfory.Int32] + f3: Dict[str, pyfory.Int32] f4: List[Bar] encoder = pyfory.encoder(Foo) diff --git a/docs/guide/python/row-format.md b/docs/guide/python/row-format.md index a9f7219e41..61266be590 100644 --- a/docs/guide/python/row-format.md +++ b/docs/guide/python/row-format.md @@ -27,13 +27,13 @@ Row format drastically reduces overhead when working with large objects where on **Key Benefits:** -| Feature | Description | -| ----------------------- | ------------------------------------------------------ | -| Zero-Copy Access | Read nested fields without deserializing entire object | -| Memory Efficiency | Memory-map large datasets directly from disk | -| Cross-Language | Binary format compatible between Python, Java, C++ | -| Partial Deserialization | Deserialize only specific elements you need | -| High Performance | Skip unnecessary data parsing for analytics workloads | +| Feature | Description | +| ----------------------- | -------------------------------------------------------- | +| Zero-Copy Access | Read nested fields without deserializing entire object | +| Memory Efficiency | Memory-map large datasets directly from disk | +| Cross-Language | Binary format compatible between Python, Java, C++, Rust | +| Partial Deserialization | Deserialize only specific elements you need | +| High Performance | Skip unnecessary data parsing for analytics workloads | ## Basic Usage @@ -102,7 +102,7 @@ standard-library Python `array.array` carrier, not PyArrow. ## Cross-Language Compatibility -Row format works seamlessly across languages. The same binary data can be accessed from Java and C++. +Row format works seamlessly across languages. The same binary data can be accessed from Java, C++, and Rust. ### Java diff --git a/docs/guide/rust/basic-serialization.md b/docs/guide/rust/basic-serialization.md index ea18eaafc8..0d1737e65e 100644 --- a/docs/guide/rust/basic-serialization.md +++ b/docs/guide/rust/basic-serialization.md @@ -152,10 +152,12 @@ fory = { version = "1.5.0", features = ["chrono"] } ### Custom Types -| Macro | Description | -| ----------------------- | -------------------------- | -| `#[derive(ForyStruct)]` | Object graph serialization | -| `#[derive(ForyRow)]` | Row-based serialization | +| Macro | Description | +| ----------------------- | ------------------------------------- | +| `#[derive(ForyStruct)]` | Object graph serialization | +| `#[derive(ForyRow)]` | Standard Row Format for named structs | + +`ForyRow` has a separate type set and returns borrowed field views. Row reads and field access use `Result` to report invalid row data. See [Row Format](row-format.md) for supported types, nullability, and examples. ## Serialization APIs @@ -205,7 +207,7 @@ all supported carriers, and registration. ## Performance Tips -- **Zero-Copy Deserialization**: Row format enables direct memory access without copying +- **Selective Zero-Copy Access**: Row Format returns borrowed views for direct field and element access - **Buffer Pre-allocation**: Minimizes memory allocations during serialization - **Compact Encoding**: Variable-length encoding for space efficiency - **Little-Endian**: Optimized for modern CPU architectures @@ -217,3 +219,4 @@ all supported carriers, and registration. - [References](references.md) - Shared and circular references - [Custom Serializers](custom-serializers.md) - Custom serialization - [External-Type Serialization](external-types.md) - Third-party values and carrier roots +- [Row Format](row-format.md) - Standard Row Format and zero-copy borrowed views diff --git a/docs/guide/rust/index.md b/docs/guide/rust/index.md index 6115e7ff7e..3f676f0dad 100644 --- a/docs/guide/rust/index.md +++ b/docs/guide/rust/index.md @@ -19,7 +19,7 @@ license: | limitations under the License. --- -**Apache Fory™** is a blazing 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. +**Apache Fory™** is a high-performance multi-language serialization framework. The Rust implementation uses compile-time code generation for object serialization and borrowed views for zero-copy Row Format access. The Rust implementation provides versatile and high-performance serialization with automatic memory management and compile-time type safety. It supports both xlang mode for cross-language payloads and native mode for Rust-only payloads. @@ -32,7 +32,7 @@ The Rust implementation provides versatile and high-performance serialization wi - **Circular references**: Automatic tracking of shared and circular references with `Rc`/`Arc` and weak pointers - **Polymorphic**: Serialize trait objects with `Box`, `Rc`, and `Arc` - **Schema evolution**: Compatible mode for independent schema changes -- **Two formats**: Object graph serialization and zero-copy row-based format +- **Two formats**: Object graph serialization and the Standard Row Format shared with Java, C++, and Python ## Crates @@ -146,30 +146,6 @@ fn main() -> Result<(), Error> { **Tip:** Perform registrations (such as `fory.register::(id)`) before spawning threads so every worker sees the same metadata. Once configured, wrapping the instance in `Arc` is enough to fan out serialization and deserialization tasks safely. -## Architecture - -The Rust implementation consists of three main crates: - -``` -fory/ # High-level API -├── src/lib.rs # Public API exports - -fory-core/ # Core serialization engine -├── src/ -│ ├── fory.rs # Main serialization entry point -│ ├── buffer.rs # Binary buffer management -│ ├── serializer/ # Type-specific serializers -│ ├── resolver/ # Type resolution and metadata -│ ├── meta/ # Meta string compression -│ ├── row/ # Row format implementation -│ └── types.rs # Type definitions - -fory-derive/ # Procedural macros -├── src/ -│ ├── object/ # ForyStruct macro -│ └── fory_row.rs # ForyRow macro -``` - ## Use Cases ### Object Serialization @@ -180,13 +156,14 @@ fory-derive/ # Procedural macros - Schema evolution with compatible mode - Graph-like data structures with circular references -### Row-Based Serialization +### Standard Row Format - High-throughput data processing - Analytics workloads requiring fast field access - Memory-constrained environments - Real-time data streaming applications -- Zero-copy scenarios +- Zero-copy field and collection access +- Standard Row Format interchange with Java, C++, and Python ## Next Steps @@ -199,5 +176,5 @@ fory-derive/ # Procedural macros - [Custom Serializers](custom-serializers.md) - Implement custom serialization behavior - [External-Type Serialization](external-types.md) - External structural and custom serializers plus carrier composition -- [Row Format](row-format.md) - Zero-copy row-based format +- [Row Format](row-format.md) - Standard Row Format with borrowed views - [gRPC Support](grpc-support.md) - Fory payloads over tonic diff --git a/docs/guide/rust/row-format.md b/docs/guide/rust/row-format.md index 9c5c545525..5ae2e3703f 100644 --- a/docs/guide/rust/row-format.md +++ b/docs/guide/rust/row-format.md @@ -19,19 +19,13 @@ license: | limitations under the License. --- -Apache Fory™ provides a high-performance **row format** for zero-copy deserialization. +Apache Fory™ Rust implements the Standard Row Format used by Java, C++, and Python. It provides zero-copy borrowed views and random field access without reconstructing the complete value. ## Overview -Unlike traditional object serialization that reconstructs entire objects in memory, row format enables **random access** to fields directly from binary data without full deserialization. +Use Row Format when readers need selected fields or collection elements rather than an owned copy of the complete value. The view borrows the input bytes, so the bytes must remain alive while the view is in use. -**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 +Row Format is schema-driven: the Rust type supplied to `from_row` determines the field types and declaration order. Cross-language readers and writers must use the same schema. ## When to Use Row Format @@ -39,88 +33,133 @@ Unlike traditional object serialization that reconstructs entire objects in memo - 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 +- Sharing Standard Row Format bytes with Java, C++, or Python ## Basic Usage ```rust -use fory::{to_row, from_row}; -use fory::ForyRow; +use fory::{from_row, to_row, Error, ForyRow}; use std::collections::BTreeMap; #[derive(ForyRow)] struct UserProfile { id: i64, username: String, - email: String, + email: Option, scores: Vec, preferences: BTreeMap, 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, -}; - -// Serialize to row format -let row_data = to_row(&profile).unwrap(); - -// Zero-copy deserialization - no object allocation! -let row = from_row::(&row_data); - -// Access fields directly from binary data -assert_eq!(row.id(), 12345); -assert_eq!(row.username(), "alice"); -assert_eq!(row.email(), "alice@example.com"); -assert_eq!(row.is_active(), true); - -// Access collections efficiently -let scores = row.scores(); -assert_eq!(scores.size(), 4); -assert_eq!(scores.get(0).unwrap(), 95); -assert_eq!(scores.get(1).unwrap(), 87); - -let prefs = row.preferences(); -assert_eq!(prefs.keys().size(), 2); -assert_eq!(prefs.keys().get(0).unwrap(), "language"); -assert_eq!(prefs.values().get(0).unwrap(), "en"); +fn main() -> Result<(), Error> { + let profile = UserProfile { + id: 12345, + username: "alice".to_string(), + email: Some("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)?; + let row = from_row::(&row_data)?; + + // Field methods return Result and validate the referenced bytes. + assert_eq!(row.id()?, 12345); + assert_eq!(row.username()?, "alice"); + assert_eq!(row.email()?, Some("alice@example.com")); + assert!(row.is_active()?); + + let scores = row.scores()?; + assert_eq!(scores.len(), 4); + assert_eq!(scores.get(0)?, 95); + assert_eq!(scores.get(1)?, 87); + + let preferences = row.preferences()?; + assert_eq!(preferences.keys().len(), 2); + assert_eq!(preferences.keys().get(0)?, "language"); + assert_eq!(preferences.values().get(0)?, "en"); + Ok(()) +} ``` -## How It Works +`to_row` accepts Row Format roots: derived structs, supported arrays, and `BTreeMap` values. Scalar, string, binary, and `Option` values are field or element values rather than standalone roots. + +## Nullability and Field Order + +`Option` declares a nullable field or array element. `None` sets the corresponding null bit, and the field method returns `None` without reading a value body. `Some(value)` uses the same fixed slot width as `T`. + +`#[derive(ForyRow)]` supports named structs, including generic structs. Fields are encoded in source declaration order. The derive generates a borrowed `StructNameRowView` type whose visibility matches the source struct. Each generated field method preserves the corresponding field's visibility and returns `Result<_, Error>`. + +Changing field order or field types changes the Row Format schema. Coordinate such changes across all producers and consumers. + +## Supported Types + +| Rust type | Standard Row Format encoding | +| ---------------------------------------------- | -------------------------------- | +| `bool`, `i8`, `i16`, `i32`, `i64` | Fixed-width scalar | +| `f32`, `f64` | Fixed-width IEEE 754 scalar | +| `Date` | Fixed-width date32 in epoch days | +| `Timestamp` | Fixed-width epoch microseconds | +| `Duration` | Fixed-width microseconds | +| `String`, `&str` | Variable-width UTF-8 | +| `Vec`, `&[u8]` | Variable-width binary | +| `Vec`, `[T; N]` for supported element types | Standard array | +| `BTreeMap` | Standard map | +| Named structs with `#[derive(ForyRow)]` | Nested Standard Row | +| `Option` | Nullable field or array element | + +`Float16` and `Decimal` are not supported because the Standard Row Format specification does not define complete interoperable encodings for them. + +Fixed arrays require the encoded element count to equal `N`. `BTreeMap` keys must implement `Ord`; map values do not need to implement `Ord`. -- Fields are encoded in a binary row with fixed offsets for primitives -- Variable-length data (strings, collections) stored with offset pointers -- Null bitmap tracks which fields are present -- Nested structures supported through recursive row encoding +`Vec` is encoded as binary rather than as a Standard Array. Use another supported element type when an array representation is required. + +## Standard Binary Layout + +- A row starts with an 8-byte-aligned null bitmap followed by one 8-byte slot per field. +- Fixed-width values are stored little-endian at the low address of their slot. Unused slot bytes are zero. +- A variable-width slot is the little-endian `u64` value `(relative_offset << 32) | size`. The value body and its zero padding follow the fixed region. +- An array starts with a `u64` element count and an 8-byte-aligned null bitmap. Fixed-width elements use contiguous natural-width slots; variable-width elements use 8-byte offset-size slots. +- A map contains the key-array byte size followed by complete key and value arrays. Nested structs, arrays, and maps are complete child structures. +- Variable bodies and array slot regions are padded with zeroes to an 8-byte boundary. Offsets are relative to the immediate containing row or array. + +For the normative layout and size formulas, see the [Row Format Specification](https://fory.apache.org/docs/specification/row_format_spec). + +## Validation and Errors + +`from_row`, generated field methods, and array `get` calls return `Result`. They reject truncated fixed regions, invalid counts, out-of-range offsets and sizes, invalid UTF-8, fixed-array length mismatches, and mismatched map key/value counts. + +Array access is also bounds-checked: + +```rust +let scores = row.scores()?; +assert!(scores.get(scores.len()).is_err()); +``` ## Performance Comparison -| Operation | Object Format | Row Format | -| -------------------- | ----------------------------- | ------------------------------- | -| Full deserialization | Allocates all objects | Zero allocation | -| Single field access | Full deserialization required | Direct offset read | -| Memory usage | Full object graph in memory | Only accessed fields in memory | -| Suitable for | Small objects, full access | Large objects, selective access | +| Operation | Object Format | Row Format | +| ----------------- | ----------------------------------- | ---------------------------------------- | +| Open encoded data | Reconstructs an owned value | Creates a borrowed view | +| Read one field | Accesses the reconstructed object | Validates and reads the field directly | +| Collection access | Uses an owned collection | Uses a borrowed array or map view | +| Suitable for | Full object use and graph semantics | Selective access and cross-language rows | ## ForyRow vs ForyStruct -| Feature | `#[derive(ForyRow)]` | `#[derive(ForyStruct)]` | -| --------------- | --------------------- | -------------------------- | -| Deserialization | Zero-copy, lazy | Full object reconstruction | -| Field access | Direct from binary | Normal struct access | -| Memory usage | Minimal | Full object | -| Best for | Analytics, large data | General serialization | +| Feature | `#[derive(ForyRow)]` | `#[derive(ForyStruct)]` | +| ------------ | -------------------------------------- | ---------------------------- | +| Read result | Borrowed view | Owned Rust value | +| Field access | Field method returning `Result` | Normal struct access | +| Schema order | Source declaration order | Object-format schema rules | +| Best for | Selective access to Standard Row bytes | General object serialization | ## Related Topics - [Basic Serialization](basic-serialization.md) - Object graph serialization -- [Xlang Serialization](xlang-serialization.md) - Row format across languages +- [Cross-Language Row Format](../xlang/row_format.md) - Row format across languages - [Row Format Specification](https://fory.apache.org/docs/specification/row_format_spec) - Protocol details diff --git a/docs/guide/xlang/row_format.md b/docs/guide/xlang/row_format.md index afc256b7b3..9ff1e4baa3 100644 --- a/docs/guide/xlang/row_format.md +++ b/docs/guide/xlang/row_format.md @@ -19,15 +19,20 @@ license: | limitations under the License. --- -Fory Row Format is a cache-friendly binary format designed for efficient random access and partial serialization. Unlike object graph serialization, row format allows you to read individual fields without deserializing the entire object. +Fory Row Format is a cache-friendly binary format designed for efficient random access. Unlike object graph serialization, Row Format allows readers to access individual fields without reconstructing the complete object. ## Features - **Zero-Copy Random Access**: Read specific fields directly from binary data -- **Partial Serialization**: Skip unnecessary fields during serialization -- **Cross-Language Compatible**: Row format data can be shared between Java, Python, and C++ +- **Selective Access**: Read only the fields or collection elements an application needs +- **Cross-Language Compatible**: Standard Row Format data can be shared between Java, Python, C++, and Rust - **Apache Arrow Integration**: Convert row format to/from Arrow RecordBatch for analytics (Java/Python) +| Format | Implementations | Purpose | +| ------------------- | ----------------------- | -------------------------- | +| Standard Row Format | Java, Python, C++, Rust | Cross-language interchange | +| Compact Row Format | Java | Smaller Java-only rows | + ## Java ```java @@ -78,15 +83,22 @@ Bar newBar2 = barEncoder.fromRow(binaryArray4.getStruct(20)); ## Python ```python +import datetime +import pickle +from dataclasses import dataclass +from typing import Dict, List + +import pyfory + @dataclass class Bar: f1: str - f2: List[pa.int64] + f2: List[pyfory.Int64] @dataclass class Foo: - f1: pa.int32 - f2: List[pa.int32] - f3: Dict[str, pa.int32] + f1: pyfory.Int32 + f2: List[pyfory.Int32] + f3: Dict[str, pyfory.Int32] f4: List[Bar] encoder = pyfory.encoder(Foo) @@ -106,6 +118,42 @@ print(new_foo.f2[100000], new_foo.f4[100000].f1, new_foo.f4[200000].f2[5]) print(f"pickle end: {datetime.datetime.now()}") ``` +## Rust + +Rust derives a static Row Format schema from a named struct. Struct fields use source declaration order, and `Option` declares nullable fields or array elements. The Rust type used by `from_row` must match the producer's field types and order. + +```rust +use fory::{from_row, to_row, Error, ForyRow}; + +#[derive(ForyRow)] +struct Metric { + id: i64, + label: String, + samples: Vec, + note: Option, +} + +fn main() -> Result<(), Error> { + let bytes = to_row(&Metric { + id: 7, + label: "latency".to_string(), + samples: vec![1.5, 2.0], + note: None, + })?; + + let row = from_row::(&bytes)?; + assert_eq!(row.id()?, 7); + assert_eq!(row.label()?, "latency"); + assert_eq!(row.note()?, None); + + let samples = row.samples()?; + assert_eq!(samples.get(1)?, 2.0); + Ok(()) +} +``` + +Rust reads and writes the exact Standard Row Format, including natural-width primitive array elements, little-endian offset-size slots, null bitmaps, and 8-byte zero padding. See the [Rust Row Format Guide](../rust/row-format.md) for supported types and API details. + ## Apache Arrow Support Fory Row Format supports automatic conversion from/to Arrow Table/RecordBatch for analytics workloads. @@ -188,3 +236,4 @@ Parent decoded = encoder.fromRow(row); - [Row Format Specification](https://fory.apache.org/docs/specification/row_format_spec) - Binary format details - [Java Row Format Guide](../java/row-format.md) - Java-specific row format documentation - [Python Row Format Guide](../python/row-format.md) - Python-specific row format documentation +- [Rust Row Format Guide](../rust/row-format.md) - Rust-specific row format documentation diff --git a/docs/specification/row_format_spec.md b/docs/specification/row_format_spec.md index de1e8b50da..6c487f39ea 100644 --- a/docs/specification/row_format_spec.md +++ b/docs/specification/row_format_spec.md @@ -26,14 +26,14 @@ Apache Fory Row Format is a cache-friendly, random-access binary format designed - **Random Field Access**: Read individual fields without deserializing the entire row - **Zero-Copy Operations**: Direct memory access without data transformation - **Cache-Friendly Layout**: Optimized memory layout for CPU cache efficiency -- **Cross-Language Support**: Consistent binary format across Java, C++, and Python +- **Cross-Language Support**: Consistent binary format across Java, C++, Python, and Rust Fory provides two row format variants: -| Format | Languages | Use Case | -| --------------- | ----------------- | ------------------------------ | -| Standard Format | Java, C++, Python | Cross-language compatibility | -| Compact Format | Java only | Space efficiency, smaller rows | +| Format | Languages | Use Case | +| --------------- | ----------------------- | ------------------------------ | +| Standard Format | Java, C++, Python, Rust | Cross-language compatibility | +| Compact Format | Java only | Space efficiency, smaller rows | ## Format Comparison @@ -99,7 +99,7 @@ Each field occupies a fixed 8-byte slot regardless of its actual data type: #### Variable-Width Data Encoding -Variable-length fields (strings, arrays, maps, nested structs) store an offset-size pair in their slot: +Variable-length fields (strings, arrays, maps, nested structs) store an offset-size pair in their slot. The pair is interpreted as one little-endian 64-bit value: ``` +---------------------------+---------------------------+ @@ -111,6 +111,7 @@ Variable-length fields (strings, arrays, maps, nested structs) store an offset-s - **Relative Offset** (upper 32 bits): Offset from the row's base address - **Size** (lower 32 bits): Size of the variable-width data in bytes +- **Physical byte order**: Bytes 0-3 contain size and bytes 4-7 contain the relative offset **Encoding**: @@ -201,6 +202,7 @@ Maps store key-value pairs as two separate arrays: **Values Array Offset**: `base_offset + 8 + keys_array_size` Both keys and values arrays follow the standard array binary layout. +The key and value arrays must contain the same number of elements. ### Nested Struct Layout @@ -445,11 +447,14 @@ where: ### Standard Array Size ``` -array_size = header_size + data_size +array_size = header_size + fixed_data_size + variable_data_size where: header_size = 8 + ((num_elements + 63) / 64) * 8 - data_size = ((num_elements * element_size + 7) / 8) * 8 + element_slot_size = natural width for fixed-width elements, otherwise 8 + fixed_data_size = ((num_elements * element_slot_size + 7) / 8) * 8 + variable_data_size = sum of (padded_size for each non-null variable-width element) + padded_size = ((size + 7) / 8) * 8 ``` ### Compact Array Size @@ -473,14 +478,14 @@ map_size = 8 + keys_array_size + values_array_size ### Layout Summary -| Component | Standard Format | Compact Format | -| ---------------- | ------------------------------- | ------------------------------------- | -| Row Header | `((N + 63) / 64) * 8` bytes | 0 or `(N + 7) / 8` bytes (at end) | -| Row Field Slots | `N * 8` bytes | `sum(field_widths)` bytes | -| Array Header | `8 + ((E + 63) / 64) * 8` bytes | `4 + (E + 7) / 8` bytes (if nullable) | -| Array Elements | `E * element_size` (8-aligned) | `E * element_width` | -| Map Header | 8 bytes | 8 bytes | -| Offset+Size Pair | 8 bytes (32-bit offset + size) | 8 bytes (same) | +| Component | Standard Format | Compact Format | +| ---------------- | -------------------------------------- | ------------------------------------- | +| Row Header | `((N + 63) / 64) * 8` bytes | 0 or `(N + 7) / 8` bytes (at end) | +| Row Field Slots | `N * 8` bytes | `sum(field_widths)` bytes | +| Array Header | `8 + ((E + 63) / 64) * 8` bytes | `4 + (E + 7) / 8` bytes (if nullable) | +| Array Elements | 8-aligned slots plus variable bodies | `E * element_width` | +| Map Header | 8 bytes | 8 bytes | +| Offset+Size Pair | 8-byte `u64`: `(offset << 32) \| size` | 8 bytes (same) | Where N = number of fields, E = number of elements @@ -505,7 +510,7 @@ Where N = number of fields, E = number of elements ### Endianness - All multi-byte integers are stored in **little-endian** format -- Floating-point values use native IEEE 754 representation +- Floating-point values use IEEE 754 bit representations stored in little-endian byte order ### Memory Safety @@ -530,11 +535,11 @@ Where N = number of fields, E = number of elements ### When to Use Each Format -| Scenario | Recommended Format | -| -------------------------------- | ------------------ | -| Cross-language data exchange | Standard | -| Java-only, memory-constrained | Compact | -| Many small primitive fields | Compact | -| Many nested fixed-size structs | Compact | -| Maximum read performance | Standard | -| Interoperability with C++/Python | Standard | +| Scenario | Recommended Format | +| ------------------------------------------ | ------------------ | +| Cross-language data exchange | Standard | +| Java-only, memory-constrained | Compact | +| Many small primitive fields | Compact | +| Many nested fixed-size structs | Compact | +| Maximum read performance | Standard | +| Interoperability with Java/C++/Python/Rust | Standard | diff --git a/rust/README.md b/rust/README.md index 89721ec4c8..f5936d3857 100644 --- a/rust/README.md +++ b/rust/README.md @@ -17,7 +17,7 @@ The Rust implementation provides versatile and high-performance serialization wi - **Polymorphic**: Serialize trait objects with `Box`, `Rc`, and `Arc` - **Schema Evolution**: Compatible mode for independent schema changes - **Reduced-Precision Types**: `Float16` and `BFloat16` scalars with `Vec` / `Vec` arrays -- **Two Formats**: Object graph serialization and zero-copy row-based format +- **Two Formats**: Object graph serialization and the Standard Row Format shared with Java, C++, and Python ## Crates @@ -523,17 +523,16 @@ Custom serializers implement the body-only `write_data` and `read_data` operations. Fory's complete-value `write` and `read` operations add reference and type-information framing. -### 8. Row-Based Serialization +### 8. Standard Row Format -Apache Fory™ provides a high-performance **row format** for zero-copy deserialization. Unlike traditional object serialization that reconstructs entire objects in memory, row format enables **random access** to fields directly from binary data without full deserialization. +Apache Fory™ Rust implements the Standard Row Format shared with Java, C++, and Python. `from_row` returns a borrowed view, so applications can validate and access selected fields or collection elements without reconstructing the complete value. **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 +- **Zero-copy access**: Read strings, binary values, and nested structures through borrowed views +- **Selective access**: Read only the fields or collection elements the application needs +- **Cross-language rows**: Exchange the same binary layout with Java, C++, and Python +- **Bounds-checked reads**: Malformed offsets, sizes, counts, and UTF-8 return `Error` **When to use row format:** @@ -541,74 +540,49 @@ Apache Fory™ provides a high-performance **row format** for zero-copy deserial - 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 - -**How it works:** - -- Fields are encoded in a binary row with fixed offsets for primitives -- Variable-length data (strings, collections) stored with offset pointers -- Null bitmap tracks which fields are present -- Nested structures supported through recursive row encoding +- Standard Row Format interchange with other Fory implementations ```rust -use fory::{to_row, from_row}; -use fory::ForyRow; -use std::collections::BTreeMap; +use fory::{from_row, to_row, Error, ForyRow}; #[derive(ForyRow)] struct UserProfile { id: i64, username: String, - email: String, + email: Option, scores: Vec, - preferences: BTreeMap, 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, -}; - -// Serialize to row format -let row_data = to_row(&profile).unwrap(); - -// Zero-copy deserialization - no object allocation! -let row = from_row::(&row_data); - -// Access fields directly from binary data -assert_eq!(row.id(), 12345); -assert_eq!(row.username(), "alice"); -assert_eq!(row.email(), "alice@example.com"); -assert_eq!(row.is_active(), true); +fn main() -> Result<(), Error> { + let bytes = to_row(&UserProfile { + id: 12345, + username: "alice".to_string(), + email: None, + scores: vec![95, 87, 92, 88], + is_active: true, + })?; + + let row = from_row::(&bytes)?; + assert_eq!(row.id()?, 12345); + assert_eq!(row.username()?, "alice"); + assert_eq!(row.email()?, None); + assert!(row.is_active()?); + + let scores = row.scores()?; + assert_eq!(scores.len(), 4); + assert_eq!(scores.get(1)?, 87); + Ok(()) +} +``` -// Access collections efficiently -let scores = row.scores(); -assert_eq!(scores.size(), 4); -assert_eq!(scores.get(0).unwrap(), 95); -assert_eq!(scores.get(1).unwrap(), 87); +`#[derive(ForyRow)]` supports named structs, including generic structs, and encodes fields in source declaration order. `Option` supplies field or array-element nullability without changing `T`'s slot width. Generated field methods and array `get` calls return `Result`, validating variable ranges and UTF-8 when accessed. -let prefs = row.preferences(); -assert_eq!(prefs.keys().size(), 2); -assert_eq!(prefs.keys().get(0).unwrap(), "language"); -assert_eq!(prefs.values().get(0).unwrap(), "en"); -``` +Supported fixed-width values are `bool`, `i8`, `i16`, `i32`, `i64`, `f32`, `f64`, `Date`, `Timestamp`, and `Duration`. Supported variable-width values are UTF-8 `String`/`&str`, binary `Vec`/`&[u8]`, fixed and dynamic arrays over supported element types, `BTreeMap`, nested derived structs, and `Option`. `Vec` uses the binary encoding rather than the Standard Array encoding. `Float16` and `Decimal` are not supported by Row Format because the standard specification does not define complete interoperable encodings for them. -**Performance comparison:** +Standard rows use an 8-byte-aligned null bitmap and one 8-byte slot per struct field. Fixed-width fields are stored little-endian in their slots. Variable-width slots encode the little-endian `u64` value `(relative_offset << 32) | size`; variable bodies and array slot regions have zero padding to 8-byte alignment. Standard arrays use natural-width storage for fixed elements, and maps contain complete key and value arrays. -| Operation | Object Format | Row Format | -| -------------------- | ----------------------------- | ------------------------------- | -| Full deserialization | Allocates all objects | Zero allocation | -| Single field access | Full deserialization required | Direct offset read | -| Memory usage | Full object graph in memory | Only accessed fields in memory | -| Suitable for | Small objects, full access | Large objects, selective access | +`to_row` accepts derived structs, supported arrays, and `BTreeMap` roots. Scalar, string, binary, and `Option` values are field or element values rather than standalone roots. See the [Rust Row Format guide](https://fory.apache.org/docs/guide/rust/row-format) and [Row Format specification](https://fory.apache.org/docs/specification/row_format_spec) for details. ## Cross-Language Serialization @@ -633,7 +607,7 @@ See [xlang_type_mapping.md](https://fory.apache.org/docs/specification/xlang_typ Apache Fory™ Rust is designed for maximum performance: -- **Zero-Copy Deserialization**: Row format enables direct memory access without copying +- **Selective Zero-Copy Access**: Row Format returns borrowed views for direct field and element access - **Buffer Pre-allocation**: Minimizes memory allocations during serialization - **Compact Encoding**: Variable-length encoding for space efficiency - **Little-Endian**: Optimized for modern CPU architectures @@ -664,7 +638,7 @@ cd benchmarks/rust - Schema evolution with compatible mode - Graph-like data structures with circular references -### Row-Based Serialization +### Standard Row Format - High-throughput data processing - Analytics workloads requiring fast field access diff --git a/rust/api-tests/facade-only/src/lib.rs b/rust/api-tests/facade-only/src/lib.rs index 06b480e49d..726d5abce5 100644 --- a/rust/api-tests/facade-only/src/lib.rs +++ b/rust/api-tests/facade-only/src/lib.rs @@ -322,19 +322,62 @@ //! value: Box, //! } //! ``` +//! +//! A generated Row view preserves each source field's visibility: +//! +//! ```compile_fail +//! use fory::{from_row, to_row}; +//! use fory_facade_api_tests::VisibilitySchema; +//! +//! let bytes = to_row(&VisibilitySchema::new(1, 2)).unwrap(); +//! let view = from_row::(&bytes).unwrap(); +//! let _ = view.hidden(); +//! ``` +//! +//! Generated public Row views and public field methods satisfy strict documentation lints: +//! +//! ``` +//! #![doc = "Generated Row view documentation check."] +//! #![deny(missing_docs)] +//! use fory::ForyRow; +//! +//! /// A documented row schema. +//! #[derive(ForyRow)] +//! pub struct DocumentedSchema { +//! /// A documented field. +//! pub value: i64, +//! } +//! ``` + +use fory::ForyRow; + +/// A public Row schema used to verify generated field visibility. +#[derive(ForyRow)] +pub struct VisibilitySchema { + /// A public field. + pub visible: i64, + hidden: i64, +} + +impl VisibilitySchema { + /// Creates a visibility test row. + pub fn new(visible: i64, hidden: i64) -> Self { + Self { visible, hidden } + } +} #[cfg(test)] mod tests { use fory::{ from_row, register_trait_type, to_row, ArcSerializer, ArcWeakSerializer, ArraySerializer, - BTreeMapSerializer, BTreeSetSerializer, BinaryHeapSerializer, BoxSerializer, Error, Fory, - ForyEnum, ForyObject, ForyRow, ForyStruct, ForyUnion, HashMapSerializer, HashSetSerializer, - LinkedListSerializer, MutexSerializer, OptionSerializer, RcSerializer, RcWeakSerializer, - ReadContext, Reader, RefCellSerializer, Serializer, VecDequeSerializer, VecSerializer, - WriteContext, + ArrayView, BTreeMapSerializer, BTreeSetSerializer, BinaryHeapSerializer, BoxSerializer, + Error, Fory, ForyEnum, ForyObject, ForyRow, ForyStruct, ForyUnion, HashMapSerializer, + HashSetSerializer, LinkedListSerializer, MapView, MutexSerializer, OptionSerializer, + RcSerializer, RcWeakSerializer, ReadContext, Reader, RefCellSerializer, Row, Serializer, + VecDequeSerializer, VecSerializer, WriteContext, }; use fory_external_model::{Command, ExternalId, Key, Marker, Point, Status, User, Value}; - use std::collections::HashMap; + use std::collections::{BTreeMap, HashMap}; use std::rc::Rc; use std::sync::Arc; @@ -434,6 +477,12 @@ mod tests { name: String, } + fn assert_row_api() {} + + fn assert_array_view(_: &ArrayView<'_, i32>) {} + + fn assert_map_view(_: &MapView<'_, String, i32>) {} + trait Animal: ForyObject { fn name(&self) -> &str; } @@ -621,14 +670,23 @@ mod tests { #[test] fn facade_row_derive_roundtrip() { + assert_row_api::(); let value = RowUser { id: 7, name: "Grace".to_string(), }; let row = to_row(&value).unwrap(); - let decoded = from_row::(&row); - assert_eq!(decoded.id(), 7); - assert_eq!(decoded.name(), "Grace"); + let decoded = from_row::(&row).unwrap(); + assert_eq!(decoded.id().unwrap(), 7); + assert_eq!(decoded.name().unwrap(), "Grace"); + + let array_bytes = to_row(&vec![1i32]).unwrap(); + let array = from_row::>(&array_bytes).unwrap(); + assert_array_view(&array); + + let map_bytes = to_row(&BTreeMap::from([("one".to_owned(), 1i32)])).unwrap(); + let map = from_row::>(&map_bytes).unwrap(); + assert_map_view(&map); } #[test] diff --git a/rust/api-tests/renamed-facade/src/lib.rs b/rust/api-tests/renamed-facade/src/lib.rs index a6a1aafdb2..54a1dbaad9 100644 --- a/rust/api-tests/renamed-facade/src/lib.rs +++ b/rust/api-tests/renamed-facade/src/lib.rs @@ -235,8 +235,8 @@ mod tests { #[test] fn renamed_facade_row_derive() { let row = to_row(&RenamedRow { id: 9 }).unwrap(); - let decoded = from_row::(&row); - assert_eq!(decoded.id(), 9); + let decoded = from_row::(&row).unwrap(); + assert_eq!(decoded.id().unwrap(), 9); } #[test] diff --git a/rust/fory-core/src/row/bit_util.rs b/rust/fory-core/src/row/bit_util.rs index bbb94e5f79..e513c39c29 100644 --- a/rust/fory-core/src/row/bit_util.rs +++ b/rust/fory-core/src/row/bit_util.rs @@ -15,8 +15,43 @@ // specific language governing permissions and limitations // under the License. +use crate::error::Error; + const WORD_SIZE: usize = 8; +const WORD_BITS: usize = WORD_SIZE * 8; + +pub(crate) fn bitmap_width(num_values: usize) -> Result { + let words = num_values + .checked_add(WORD_BITS - 1) + .ok_or_else(|| Error::invalid_data("row bitmap width overflow"))? + / WORD_BITS; + words + .checked_mul(WORD_SIZE) + .ok_or_else(|| Error::invalid_data("row bitmap width overflow")) +} + +pub(crate) fn round_up_to_word(size: usize) -> Result { + size.checked_add(WORD_SIZE - 1) + .map(|value| value & !(WORD_SIZE - 1)) + .ok_or_else(|| Error::invalid_data("row size alignment overflow")) +} + +pub(crate) fn slot_width(fixed_size: Option) -> Result { + match fixed_size { + Some(width) if matches!(width, 1 | 2 | 4 | 8) => Ok(width), + Some(_) => Err(Error::invalid_data( + "row fixed-width values must occupy 1, 2, 4, or 8 bytes", + )), + None => Ok(8), + } +} + +#[inline(always)] +pub(crate) fn is_bit_set(bitmap: &[u8], index: usize) -> bool { + bitmap[index >> 3] & (1 << (index & 7)) != 0 +} -pub fn calculate_bitmap_width_in_bytes(num_fields: usize) -> usize { - ((num_fields + 63) / 64) * WORD_SIZE +#[inline(always)] +pub(crate) fn set_bit(bitmap: &mut [u8], index: usize) { + bitmap[index >> 3] |= 1 << (index & 7); } diff --git a/rust/fory-core/src/row/mod.rs b/rust/fory-core/src/row/mod.rs index 6c6d66f0aa..7cf81900e2 100644 --- a/rust/fory-core/src/row/mod.rs +++ b/rust/fory-core/src/row/mod.rs @@ -21,6 +21,12 @@ mod reader; mod row; mod writer; -pub use reader::{from_row, ArrayViewer, StructViewer}; +#[doc(hidden)] +pub use reader::StructView; +pub use reader::{from_row, ArrayView, MapView}; pub use row::Row; -pub use writer::{to_row, ArrayWriter, StructWriter}; +#[doc(hidden)] +pub use row::RowValue; +pub use writer::to_row; +#[doc(hidden)] +pub use writer::{StructWriter, ValueWriter}; diff --git a/rust/fory-core/src/row/reader.rs b/rust/fory-core/src/row/reader.rs index 1549a55ea7..613fe49387 100644 --- a/rust/fory-core/src/row/reader.rs +++ b/rust/fory-core/src/row/reader.rs @@ -15,110 +15,253 @@ // specific language governing permissions and limitations // under the License. -use super::{bit_util::calculate_bitmap_width_in_bytes, row::Row}; -use byteorder::{ByteOrder, LittleEndian}; +use std::collections::BTreeMap; +use std::marker::PhantomData; -struct FieldAccessorHelper<'a> { - row: &'a [u8], - get_field_offset: Box usize>, -} +use crate::error::Error; -impl<'a> FieldAccessorHelper<'a> { - fn get_offset_size(&self, idx: usize) -> (u32, u32) { - let row = self.row; - let field_offset = (self.get_field_offset)(idx); - let offset = LittleEndian::read_u32(&row[field_offset..field_offset + 4]); - let size = LittleEndian::read_u32(&row[field_offset + 4..field_offset + 8]); - (offset, size) - } - - pub fn new( - row: &'a [u8], - get_field_offset: Box usize>, - ) -> FieldAccessorHelper<'a> { - FieldAccessorHelper { - row, - get_field_offset, - } - } +use super::bit_util::{bitmap_width, is_bit_set, round_up_to_word, slot_width}; +use super::row::{Row, RowValue}; - pub fn get_field_bytes(&self, idx: usize) -> &'a [u8] { - let row = self.row; - let (offset, size) = self.get_offset_size(idx); - &row[(offset as usize)..(offset + size) as usize] - } +/// A zero-copy view over one Standard Row Format struct. +/// +/// This type is public only because `ForyRow` views are generated in +/// downstream crates. +#[doc(hidden)] +pub struct StructView<'a> { + bytes: &'a [u8], + bitmap_width: usize, + num_fields: usize, + fixed_end: usize, } -pub struct StructViewer<'r> { - field_accessor_helper: FieldAccessorHelper<'r>, -} +impl<'a> StructView<'a> { + /// Validates the fixed region for a struct with `num_fields` fields. + pub fn new(bytes: &'a [u8], num_fields: usize) -> Result { + let bitmap_width = bitmap_width(num_fields)?; + let slots_size = num_fields + .checked_mul(8) + .ok_or_else(|| Error::invalid_data("row fixed region size overflow"))?; + let fixed_end = bitmap_width + .checked_add(slots_size) + .ok_or_else(|| Error::invalid_data("row fixed region size overflow"))?; + ensure_range(bytes, 0, fixed_end)?; + Ok(Self { + bytes, + bitmap_width, + num_fields, + fixed_end, + }) + } -impl<'r> StructViewer<'r> { - pub fn new(row: &'r [u8], num_fields: usize) -> StructViewer<'r> { - let bit_map_width_in_bytes = calculate_bitmap_width_in_bytes(num_fields); - StructViewer { - field_accessor_helper: FieldAccessorHelper::new( - row, - Box::new(move |idx: usize| bit_map_width_in_bytes + idx * 8), - ), + /// Reads a field at its schema ordinal. + pub fn get(&self, index: usize) -> Result, Error> { + self.check_index(index)?; + let bitmap = &self.bytes[..self.bitmap_width]; + if is_bit_set(bitmap, index) { + return T::read_null(); } + + let slot_offset = self + .bitmap_width + .checked_add(index * 8) + .ok_or_else(|| Error::invalid_data("row field offset overflow"))?; + let value = match T::FIXED_SIZE { + Some(width) => { + slot_width(Some(width))?; + checked_slice(self.bytes, slot_offset, width)? + } + None => variable_slice(self.bytes, slot_offset, self.fixed_end)?, + }; + T::read(value) + } + + /// Returns whether a field's null bit is set. + pub fn is_null(&self, index: usize) -> Result { + self.check_index(index)?; + Ok(is_bit_set(&self.bytes[..self.bitmap_width], index)) } - pub fn get_field_bytes(&self, idx: usize) -> &'r [u8] { - self.field_accessor_helper.get_field_bytes(idx) + fn check_index(&self, index: usize) -> Result<(), Error> { + if index >= self.num_fields { + Err(Error::buffer_out_of_bound(index, 1, self.num_fields)) + } else { + Ok(()) + } } } -pub struct ArrayViewer<'r> { +/// A zero-copy view over one Standard Row Format array. +pub struct ArrayView<'a, T: RowValue> { + bytes: &'a [u8], num_elements: usize, - field_accessor_helper: FieldAccessorHelper<'r>, + bitmap_width: usize, + header_size: usize, + element_size: usize, + fixed_end: usize, + marker: PhantomData, } -impl<'r> ArrayViewer<'r> { - pub fn new(row: &'r [u8]) -> ArrayViewer<'r> { - let num_elements = LittleEndian::read_u64(&row[0..8]) as usize; - let bit_map_width_in_bytes = calculate_bitmap_width_in_bytes(num_elements); - ArrayViewer { +impl<'a, T: RowValue> ArrayView<'a, T> { + pub(crate) fn new(bytes: &'a [u8]) -> Result { + let count = read_u64(bytes, 0)?; + let num_elements = usize::try_from(count) + .map_err(|_| Error::invalid_data("row array element count exceeds usize"))?; + let bitmap_width = bitmap_width(num_elements)?; + let header_size = 8usize + .checked_add(bitmap_width) + .ok_or_else(|| Error::invalid_data("row array header size overflow"))?; + let element_size = slot_width(T::FIXED_SIZE)?; + let element_bytes = num_elements + .checked_mul(element_size) + .ok_or_else(|| Error::invalid_data("row array fixed region size overflow"))?; + let aligned_element_bytes = round_up_to_word(element_bytes)?; + let fixed_end = header_size + .checked_add(aligned_element_bytes) + .ok_or_else(|| Error::invalid_data("row array fixed region size overflow"))?; + ensure_range(bytes, 0, fixed_end)?; + Ok(Self { + bytes, num_elements, - field_accessor_helper: FieldAccessorHelper::new( - row, - Box::new(move |idx: usize| 8 + bit_map_width_in_bytes + idx * 8), - ), - } + bitmap_width, + header_size, + element_size, + fixed_end, + marker: PhantomData, + }) } - pub fn num_elements(&self) -> usize { + /// Returns the number of elements encoded in this array. + pub fn len(&self) -> usize { self.num_elements } - pub fn get_field_bytes(&self, idx: usize) -> &'r [u8] { - self.field_accessor_helper.get_field_bytes(idx) + /// Returns true when this array contains no elements. + pub fn is_empty(&self) -> bool { + self.num_elements == 0 + } + + /// Reads one array element without materializing the rest of the array. + pub fn get(&self, index: usize) -> Result, Error> { + self.check_index(index)?; + let bitmap = &self.bytes[8..8 + self.bitmap_width]; + if is_bit_set(bitmap, index) { + return T::read_null(); + } + let slot_offset = self + .header_size + .checked_add(index * self.element_size) + .ok_or_else(|| Error::invalid_data("row array element offset overflow"))?; + let value = match T::FIXED_SIZE { + Some(width) => checked_slice(self.bytes, slot_offset, width)?, + None => variable_slice(self.bytes, slot_offset, self.fixed_end)?, + }; + T::read(value) + } + + /// Returns whether an element's null bit is set. + pub fn is_null(&self, index: usize) -> Result { + self.check_index(index)?; + let bitmap_start = 8; + let bitmap = &self.bytes[bitmap_start..bitmap_start + self.bitmap_width]; + Ok(is_bit_set(bitmap, index)) + } + + fn check_index(&self, index: usize) -> Result<(), Error> { + if index >= self.num_elements { + Err(Error::buffer_out_of_bound(index, 1, self.num_elements)) + } else { + Ok(()) + } } } -pub struct MapViewer<'r> { - key_row: &'r [u8], - value_row: &'r [u8], +/// A zero-copy view over one Standard Row Format map. +pub struct MapView<'a, K: RowValue, V: RowValue> { + keys: ArrayView<'a, K>, + values: ArrayView<'a, V>, } -impl<'r> MapViewer<'r> { - pub fn new(row: &'r [u8]) -> MapViewer<'r> { - let key_byte_size = LittleEndian::read_u64(&row[0..8]) as usize; - MapViewer { - value_row: &row[key_byte_size + 8..row.len()], - key_row: &row[8..key_byte_size + 8], +impl<'a, K: RowValue, V: RowValue> MapView<'a, K, V> { + pub(crate) fn new(bytes: &'a [u8]) -> Result { + let keys_size = read_u64(bytes, 0)?; + let keys_size = usize::try_from(keys_size) + .map_err(|_| Error::invalid_data("row map key array size exceeds usize"))?; + let keys_end = 8usize + .checked_add(keys_size) + .ok_or_else(|| Error::invalid_data("row map key array size overflow"))?; + ensure_range(bytes, 8, keys_size)?; + let keys = ArrayView::::new(&bytes[8..keys_end])?; + let values = ArrayView::::new(&bytes[keys_end..])?; + if keys.len() != values.len() { + return Err(Error::invalid_data( + "row map key and value arrays have different lengths", + )); + } + Ok(Self { keys, values }) + } + + /// Returns the map's key array. + pub fn keys(&self) -> &ArrayView<'a, K> { + &self.keys + } + + /// Returns the map's value array. + pub fn values(&self) -> &ArrayView<'a, V> { + &self.values + } + + /// Materializes this view as a `BTreeMap`. + pub fn to_btree_map( + &self, + ) -> Result::View<'a>, ::View<'a>>, Error> + where + ::View<'a>: Ord, + { + let mut map = BTreeMap::new(); + for index in 0..self.keys.len() { + map.insert(self.keys.get(index)?, self.values.get(index)?); } + Ok(map) } +} - pub fn get_key_row(&self) -> &[u8] { - self.key_row +fn variable_slice(bytes: &[u8], slot_offset: usize, fixed_end: usize) -> Result<&[u8], Error> { + let offset_and_size = read_u64(bytes, slot_offset)?; + let relative_offset = usize::try_from(offset_and_size >> 32) + .map_err(|_| Error::invalid_data("row variable offset exceeds usize"))?; + let size = (offset_and_size as u32) as usize; + if relative_offset < fixed_end { + return Err(Error::invalid_data( + "row variable value overlaps the fixed region", + )); } + checked_slice(bytes, relative_offset, size) +} - pub fn get_value_row(&self) -> &[u8] { - self.value_row +fn read_u64(bytes: &[u8], offset: usize) -> Result { + let value = checked_slice(bytes, offset, 8)?; + let mut array = [0u8; 8]; + array.copy_from_slice(value); + Ok(u64::from_le_bytes(array)) +} + +fn checked_slice(bytes: &[u8], offset: usize, size: usize) -> Result<&[u8], Error> { + let end = offset + .checked_add(size) + .ok_or_else(|| Error::buffer_out_of_bound(offset, size, bytes.len()))?; + if end > bytes.len() { + Err(Error::buffer_out_of_bound(offset, size, bytes.len())) + } else { + Ok(&bytes[offset..end]) } } -pub fn from_row<'a, T: Row<'a>>(row: &'a [u8]) -> T::ReadResult { - T::cast(row) +fn ensure_range(bytes: &[u8], offset: usize, size: usize) -> Result<(), Error> { + checked_slice(bytes, offset, size).map(|_| ()) +} + +/// Decodes a Standard Row Format struct, array, or map root. +pub fn from_row(bytes: &[u8]) -> Result, Error> { + T::read(bytes) } diff --git a/rust/fory-core/src/row/row.rs b/rust/fory-core/src/row/row.rs index 8f6e5181ca..79ce824ea4 100644 --- a/rust/fory-core/src/row/row.rs +++ b/rust/fory-core/src/row/row.rs @@ -15,301 +15,343 @@ // specific language governing permissions and limitations // under the License. -use crate::types::{Date, Duration, Timestamp}; -use crate::{buffer::Writer, error::Error}; -use byteorder::{ByteOrder, LittleEndian}; use std::collections::BTreeMap; -use std::marker::PhantomData; -use super::{ - reader::{ArrayViewer, MapViewer}, - writer::{ArrayWriter, MapWriter}, -}; +use crate::error::Error; +use crate::types::{Date, Duration, Timestamp}; -pub trait Row<'a> { - type ReadResult; +use super::reader::{ArrayView, MapView}; +use super::writer::{ArrayWriter, MapWriter, ValueWriter}; - fn write(v: &Self, writer: &mut Writer) -> Result<(), Error>; +/// Static Row Format behavior for one schema value. +/// +/// This trait is public because `ForyRow` implementations are generated in +/// downstream crates. Most applications should derive `ForyRow` instead of +/// implementing it directly. +#[doc(hidden)] +pub trait RowValue { + /// Zero-copy projection returned when this value is read. + type View<'a>; - fn cast(bytes: &'a [u8]) -> Self::ReadResult; -} + /// Natural fixed width, or `None` for an offset-addressed value. + const FIXED_SIZE: Option; -fn read_i8_from_bytes(bytes: &[u8]) -> i8 { - bytes[0] as i8 + /// Writes exactly one value to its container-selected destination. + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error>; + + /// Reads exactly one value from its container-resolved bytes. + fn read<'a>(bytes: &'a [u8]) -> Result, Error>; + + /// Returns true when this value should set its container null bit. + fn is_null(&self) -> bool { + false + } + + /// Produces the projection for a set null bit. + fn read_null<'a>() -> Result, Error> { + Err(Error::invalid_data( + "null row value cannot be read as a non-optional type", + )) + } } -macro_rules! impl_row_for_number { - ($tt: tt, $writer: expr ,$visitor: expr) => { - impl<'a> Row<'a> for $tt { - type ReadResult = Self; +/// A self-contained Standard Row Format root. +/// +/// Derived structs, arrays, and maps implement this marker. Scalar, string, +/// binary, and optional values are field/element values rather than row roots. +pub trait Row: RowValue {} + +macro_rules! impl_fixed_row_value { + ($ty:ty, $size:expr) => { + impl RowValue for $ty { + type View<'a> = Self; - fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - $writer(writer, *v); - Ok(()) + const FIXED_SIZE: Option = Some($size); + + #[inline(always)] + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + writer.write_bytes(&self.to_le_bytes()) } - fn cast(bytes: &[u8]) -> Self::ReadResult { - $visitor(bytes) + #[inline(always)] + fn read(bytes: &[u8]) -> Result { + Ok(Self::from_le_bytes(read_fixed(bytes)?)) } } }; } -impl_row_for_number!(i8, Writer::write_i8, read_i8_from_bytes); -impl_row_for_number!(i16, Writer::write_i16, LittleEndian::read_i16); -impl_row_for_number!(i32, Writer::write_i32, LittleEndian::read_i32); -impl_row_for_number!(i64, Writer::write_i64, LittleEndian::read_i64); -impl_row_for_number!(f32, Writer::write_f32, LittleEndian::read_f32); -impl_row_for_number!(f64, Writer::write_f64, LittleEndian::read_f64); - -impl<'a> Row<'a> for String { - type ReadResult = &'a str; - - fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - writer.write_bytes(v.as_bytes()); - Ok(()) + +impl RowValue for bool { + type View<'a> = Self; + + const FIXED_SIZE: Option = Some(1); + + #[inline(always)] + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + writer.write_bytes(&[u8::from(*self)]) } - fn cast(bytes: &'a [u8]) -> Self::ReadResult { - unsafe { std::str::from_utf8_unchecked(bytes) } + #[inline(always)] + fn read(bytes: &[u8]) -> Result { + match read_fixed::<1>(bytes)?[0] { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(Error::invalid_data("row boolean must be encoded as 0 or 1")), + } } } -impl Row<'_> for bool { - type ReadResult = Self; +impl RowValue for i8 { + type View<'a> = Self; - fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - writer.write_u8(if *v { 1 } else { 0 }); - Ok(()) + const FIXED_SIZE: Option = Some(1); + + #[inline(always)] + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + writer.write_bytes(&self.to_le_bytes()) } - fn cast(bytes: &[u8]) -> Self::ReadResult { - bytes[0] == 1 + #[inline(always)] + fn read(bytes: &[u8]) -> Result { + Ok(Self::from_le_bytes(read_fixed(bytes)?)) } } -/// ArrayGetter for fixed-size arrays, wrapping the underlying ArrayViewer -pub struct FixedArrayGetter<'a, T, const N: usize> { - array_data: ArrayViewer<'a>, - _marker: PhantomData, +impl_fixed_row_value!(i16, 2); +impl_fixed_row_value!(i32, 4); +impl_fixed_row_value!(i64, 8); +impl_fixed_row_value!(f32, 4); +impl_fixed_row_value!(f64, 8); + +impl RowValue for String { + type View<'a> = &'a str; + + const FIXED_SIZE: Option = None; + + #[inline(always)] + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + writer.write_bytes(self.as_bytes()) + } + + #[inline] + fn read(bytes: &[u8]) -> Result<&str, Error> { + std::str::from_utf8(bytes).map_err(|_| Error::invalid_data("invalid UTF-8 in row string")) + } } -impl<'a, T: Row<'a>, const N: usize> FixedArrayGetter<'a, T, N> { - pub fn size(&self) -> usize { - self.array_data.num_elements() +impl RowValue for &str { + type View<'a> = &'a str; + + const FIXED_SIZE: Option = None; + + #[inline(always)] + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + writer.write_bytes(self.as_bytes()) } - pub fn get(&self, idx: usize) -> Result { - if idx >= self.array_data.num_elements() { - return Err(Error::buffer_out_of_bound( - idx, - 1, - self.array_data.num_elements(), - )); - } - let bytes = self.array_data.get_field_bytes(idx); - Ok(::cast(bytes)) + #[inline] + fn read(bytes: &[u8]) -> Result<&str, Error> { + std::str::from_utf8(bytes).map_err(|_| Error::invalid_data("invalid UTF-8 in row string")) } } -impl<'a, T: Row<'a>, const N: usize> Row<'a> for [T; N] { - type ReadResult = FixedArrayGetter<'a, T, N>; +impl RowValue for Vec { + type View<'a> = &'a [u8]; - fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - let mut array_writer = ArrayWriter::new(N, writer)?; - for (idx, item) in v.iter().enumerate() { - let callback_info = array_writer.write_start(idx); - ::write(item, array_writer.get_writer())?; - array_writer.write_end(callback_info); - } - Ok(()) + const FIXED_SIZE: Option = None; + + #[inline(always)] + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + writer.write_bytes(self) } - fn cast(row: &'a [u8]) -> Self::ReadResult { - FixedArrayGetter { - array_data: ArrayViewer::new(row), - _marker: PhantomData::, - } + #[inline(always)] + fn read(bytes: &[u8]) -> Result<&[u8], Error> { + Ok(bytes) } } -impl Row<'_> for Date { - type ReadResult = Result; +impl RowValue for &[u8] { + type View<'a> = &'a [u8]; - fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - let days = i32::try_from(v.epoch_days()).map_err(|_| { - Error::invalid_data(format!( - "row date day count {} exceeds date32 range", - v.epoch_days() - )) - })?; - writer.write_i32(days); - Ok(()) + const FIXED_SIZE: Option = None; + + #[inline(always)] + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + writer.write_bytes(self) } - fn cast(bytes: &[u8]) -> Self::ReadResult { - Ok(Date::from_epoch_days(i64::from(LittleEndian::read_i32( - bytes, - )))) + #[inline(always)] + fn read(bytes: &[u8]) -> Result<&[u8], Error> { + Ok(bytes) } } -impl Row<'_> for Timestamp { - type ReadResult = Result; +impl RowValue for Option { + type View<'a> = Option>; - fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - writer.write_i64(v.to_epoch_micros()?); - Ok(()) + const FIXED_SIZE: Option = T::FIXED_SIZE; + + #[inline(always)] + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + match self { + Some(value) => value.write(writer), + None => Err(Error::invalid_data( + "a null row value must be written by its container", + )), + } } - fn cast(bytes: &[u8]) -> Self::ReadResult { - Ok(Timestamp::from_epoch_micros(LittleEndian::read_i64(bytes))) + #[inline(always)] + fn read<'a>(bytes: &'a [u8]) -> Result, Error> { + T::read(bytes).map(Some) + } + + #[inline(always)] + fn is_null(&self) -> bool { + self.is_none() + } + + #[inline(always)] + fn read_null<'a>() -> Result, Error> { + Ok(None) } } -impl Row<'_> for Duration { - type ReadResult = Result; +impl RowValue for Date { + type View<'a> = Self; - fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - writer.write_i64(v.to_micros()?); - Ok(()) + const FIXED_SIZE: Option = Some(4); + + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + let days = i32::try_from(self.epoch_days()).map_err(|_| { + Error::invalid_data(format!( + "row date day count {} exceeds date32 range", + self.epoch_days() + )) + })?; + writer.write_bytes(&days.to_le_bytes()) } - fn cast(bytes: &[u8]) -> Self::ReadResult { - Ok(Duration::from_micros(LittleEndian::read_i64(bytes))) + fn read(bytes: &[u8]) -> Result { + let days = i32::from_le_bytes(read_fixed(bytes)?); + Ok(Date::from_epoch_days(i64::from(days))) } } -impl<'a> Row<'a> for Vec { - type ReadResult = &'a [u8]; +impl RowValue for Timestamp { + type View<'a> = Self; - fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> { - writer.write_bytes(v); - Ok(()) + const FIXED_SIZE: Option = Some(8); + + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + writer.write_bytes(&self.to_epoch_micros()?.to_le_bytes()) } - fn cast(bytes: &'a [u8]) -> Self::ReadResult { - bytes + fn read(bytes: &[u8]) -> Result { + Ok(Timestamp::from_epoch_micros(i64::from_le_bytes( + read_fixed(bytes)?, + ))) } } -pub struct ArrayGetter<'a, T> { - array_data: ArrayViewer<'a>, - _marker: PhantomData, +impl RowValue for Duration { + type View<'a> = Self; + + const FIXED_SIZE: Option = Some(8); + + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + writer.write_bytes(&self.to_micros()?.to_le_bytes()) + } + + fn read(bytes: &[u8]) -> Result { + Ok(Duration::from_micros(i64::from_le_bytes(read_fixed( + bytes, + )?))) + } } -#[allow(clippy::needless_lifetimes)] -impl<'a, T: Row<'a>> ArrayGetter<'a, T> { - pub fn size(&self) -> usize { - self.array_data.num_elements() +impl RowValue for [T; N] { + type View<'a> = ArrayView<'a, T>; + + const FIXED_SIZE: Option = None; + + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + let mut array_writer = ArrayWriter::::new(N, writer.into_variable()?)?; + for (index, value) in self.iter().enumerate() { + array_writer.write(index, value)?; + } + Ok(()) } - pub fn get(&self, idx: usize) -> Result { - if idx >= self.array_data.num_elements() { - return Err(Error::buffer_out_of_bound( - idx, - 1, - self.array_data.num_elements(), - )); + fn read(bytes: &[u8]) -> Result, Error> { + let view = ArrayView::new(bytes)?; + if view.len() != N { + return Err(Error::invalid_data(format!( + "row fixed array expected {N} elements, found {}", + view.len() + ))); } - let bytes = self.array_data.get_field_bytes(idx); - Ok(::cast(bytes)) + Ok(view) } } -#[allow(clippy::needless_lifetimes)] -impl<'a, T: Row<'a>> Row<'a> for Vec { - type ReadResult = ArrayGetter<'a, T>; +impl Row for [T; N] {} + +impl RowValue for Vec { + type View<'a> = ArrayView<'a, T>; + + const FIXED_SIZE: Option = None; - fn write<'b>(v: &Self, writer: &mut Writer<'b>) -> Result<(), Error> { - let mut array_writer = ArrayWriter::new(v.len(), writer)?; - for (idx, item) in v.iter().enumerate() { - let callback_info = array_writer.write_start(idx); - ::write(item, array_writer.get_writer())?; - array_writer.write_end(callback_info); + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + let mut array_writer = ArrayWriter::::new(self.len(), writer.into_variable()?)?; + for (index, value) in self.iter().enumerate() { + array_writer.write(index, value)?; } Ok(()) } - fn cast(row: &'a [u8]) -> Self::ReadResult { - ArrayGetter { - array_data: ArrayViewer::new(row), - _marker: PhantomData::, - } + fn read(bytes: &[u8]) -> Result, Error> { + ArrayView::new(bytes) } } -pub struct MapGetter<'a, T1, T2> +impl Row for Vec {} + +impl RowValue for BTreeMap where - T1: Ord, - T2: Ord, + K: RowValue + Ord, + V: RowValue, { - map_data: MapViewer<'a>, - _key_marker: PhantomData, - _value_marker: PhantomData, -} + type View<'a> = MapView<'a, K, V>; -impl<'a, T1: Row<'a> + Ord, T2: Row<'a> + Ord> MapGetter<'a, T1, T2> { - pub fn to_btree_map(&'a self) -> Result, Error> - where - >::ReadResult: Ord, - { - let mut map = BTreeMap::new(); - let keys = self.keys(); - let values = self.values(); - - for i in 0..self.keys().size() { - map.insert(keys.get(i)?, values.get(i)?); - } - Ok(map) - } + const FIXED_SIZE: Option = None; - pub fn keys(&'a self) -> ArrayGetter<'a, T1> { - ArrayGetter { - array_data: ArrayViewer::new(self.map_data.get_key_row()), - _marker: PhantomData::, - } + fn write(&self, writer: ValueWriter<'_, '_>) -> Result<(), Error> { + let mut map_writer = MapWriter::new(writer.into_variable()?); + map_writer.write(self) } - pub fn values(&'a self) -> ArrayGetter<'a, T2> { - ArrayGetter { - array_data: ArrayViewer::new(self.map_data.get_value_row()), - _marker: PhantomData::, - } + fn read(bytes: &[u8]) -> Result, Error> { + MapView::new(bytes) } } -#[allow(clippy::needless_lifetimes)] -impl<'a, T1: Row<'a> + Ord, T2: Row<'a> + Ord> Row<'a> for BTreeMap { - type ReadResult = MapGetter<'a, T1, T2>; - - fn write<'b>(v: &Self, writer: &mut Writer<'b>) -> Result<(), Error> { - let mut map_writer = MapWriter::new(writer); - { - let callback_info = map_writer.write_start(0); - let mut array_writer = ArrayWriter::new(v.len(), map_writer.get_writer())?; - for (idx, item) in v.keys().enumerate() { - let callback_info = array_writer.write_start(idx); - ::write(item, array_writer.get_writer())?; - array_writer.write_end(callback_info); - } - map_writer.write_end(callback_info); - } - { - let mut array_writer = ArrayWriter::new(v.len(), map_writer.get_writer())?; - for (idx, item) in v.values().enumerate() { - let callback_info = array_writer.write_start(idx); - ::write(item, array_writer.get_writer())?; - array_writer.write_end(callback_info); - } - } - Ok(()) - } +impl Row for BTreeMap +where + K: RowValue + Ord, + V: RowValue, +{ +} - fn cast(row: &'a [u8]) -> Self::ReadResult { - MapGetter { - map_data: MapViewer::new(row), - _key_marker: PhantomData::, - _value_marker: PhantomData::, - } +fn read_fixed(bytes: &[u8]) -> Result<[u8; N], Error> { + if bytes.len() != N { + return Err(Error::invalid_data(format!( + "row fixed-width value expected {N} bytes, found {}", + bytes.len() + ))); } + let mut value = [0u8; N]; + value.copy_from_slice(bytes); + Ok(value) } diff --git a/rust/fory-core/src/row/writer.rs b/rust/fory-core/src/row/writer.rs index 06bf5dd3c9..41f13f8aa6 100644 --- a/rust/fory-core/src/row/writer.rs +++ b/rust/fory-core/src/row/writer.rs @@ -15,178 +15,313 @@ // specific language governing permissions and limitations // under the License. -use super::{bit_util::calculate_bitmap_width_in_bytes, row::Row}; +use std::collections::BTreeMap; +use std::marker::PhantomData; + use crate::buffer::Writer; use crate::error::Error; -pub struct WriteCallbackInfo { - field_offset: usize, - data_start: usize, +use super::bit_util::{bitmap_width, round_up_to_word, set_bit, slot_width}; +use super::row::{Row, RowValue}; + +enum WriteTarget<'a, 'b> { + Fixed(&'a mut [u8]), + Variable(&'a mut Writer<'b>), } -struct FieldWriterHelper<'a, 'b> { - pub writer: &'a mut Writer<'b>, - base_offset: usize, - get_field_offset: Box usize>, +/// The exact destination for one Row Format value. +/// +/// This type is public only because `ForyRow` implementations are generated in +/// downstream crates. Applications should use [`to_row`] instead. +#[doc(hidden)] +pub struct ValueWriter<'a, 'b> { + target: WriteTarget<'a, 'b>, } -impl<'b: 'a, 'a> FieldWriterHelper<'a, 'b> { - fn new( - writer: &'a mut Writer<'b>, - base_offset: usize, - get_field_offset: Box usize>, - ) -> FieldWriterHelper<'a, 'b> { - FieldWriterHelper { - writer, - base_offset, - get_field_offset, +impl<'a, 'b> ValueWriter<'a, 'b> { + pub(crate) fn fixed(bytes: &'a mut [u8]) -> Self { + Self { + target: WriteTarget::Fixed(bytes), + } + } + + pub(crate) fn variable(writer: &'a mut Writer<'b>) -> Self { + Self { + target: WriteTarget::Variable(writer), } } - fn write_start(&mut self, idx: usize) -> WriteCallbackInfo { - let base_offset = self.base_offset; - let field_offset = (self.get_field_offset)(idx); - let writer: &mut Writer = self.writer; - let offset = writer.len() - base_offset; - writer.set_bytes(field_offset, &(offset as u32).to_le_bytes()); - let data_start: usize = writer.len(); - WriteCallbackInfo { - field_offset, - data_start, + /// Writes the complete bytes of a scalar, string, or binary value. + pub fn write_bytes(self, bytes: &[u8]) -> Result<(), Error> { + match self.target { + WriteTarget::Fixed(output) => { + if output.len() != bytes.len() { + return Err(Error::invalid_data("row fixed-width value size mismatch")); + } + output.copy_from_slice(bytes); + } + WriteTarget::Variable(writer) => { + writer.write_bytes(bytes); + } } + Ok(()) + } + + /// Starts a derived struct at this value's variable destination. + pub fn struct_writer(self, num_fields: usize) -> Result, Error> { + StructWriter::new(num_fields, self.into_variable()?) } - fn write_end(&mut self, callback_info: WriteCallbackInfo) { - let writer: &mut Writer = self.writer; - let size: usize = writer.len() - callback_info.data_start; - writer.set_bytes(callback_info.field_offset + 4, &(size as u32).to_le_bytes()); + pub(crate) fn into_variable(self) -> Result<&'a mut Writer<'b>, Error> { + match self.target { + WriteTarget::Variable(writer) => Ok(writer), + WriteTarget::Fixed(_) => Err(Error::invalid_data( + "variable row value cannot use a fixed-width slot", + )), + } } } +/// Writes one standard Row Format struct into a shared root buffer. +/// +/// This type is public only for code generated by `ForyRow`. +#[doc(hidden)] pub struct StructWriter<'a, 'b> { - field_writer_helper: FieldWriterHelper<'a, 'b>, + writer: &'a mut Writer<'b>, + base_offset: usize, + bitmap_width: usize, + num_fields: usize, + fixed_end: usize, } impl<'a, 'b> StructWriter<'a, 'b> { - fn get_fixed_size(bit_map_width_in_bytes: usize, num_fields: usize) -> usize { - bit_map_width_in_bytes + num_fields * 8 - } - pub fn new(num_fields: usize, writer: &'a mut Writer<'b>) -> StructWriter<'a, 'b> { + fn new(num_fields: usize, writer: &'a mut Writer<'b>) -> Result { let base_offset = writer.len(); - let bit_map_width_in_bytes = calculate_bitmap_width_in_bytes(num_fields); - - let struct_writer = StructWriter { - field_writer_helper: FieldWriterHelper::new( - writer, - base_offset, - Box::new(move |idx| base_offset + bit_map_width_in_bytes + idx * 8), - ), - }; - let fixed_size = Self::get_fixed_size(bit_map_width_in_bytes, num_fields); - struct_writer.field_writer_helper.writer.reserve(fixed_size); - struct_writer.field_writer_helper.writer.skip(fixed_size); - struct_writer - } - - pub fn get_writer(&mut self) -> &mut Writer<'b> { - self.field_writer_helper.writer + let bitmap_width = bitmap_width(num_fields)?; + let slots_size = num_fields + .checked_mul(8) + .ok_or_else(|| Error::invalid_data("row fixed region size overflow"))?; + let fixed_size = bitmap_width + .checked_add(slots_size) + .ok_or_else(|| Error::invalid_data("row fixed region size overflow"))?; + let fixed_end = base_offset + .checked_add(fixed_size) + .ok_or_else(|| Error::invalid_data("row fixed region size overflow"))?; + writer.skip(fixed_size); + Ok(Self { + writer, + base_offset, + bitmap_width, + num_fields, + fixed_end, + }) } - pub fn write_start(&mut self, idx: usize) -> WriteCallbackInfo { - self.field_writer_helper.write_start(idx) - } + /// Writes a field at its schema ordinal. + pub fn write(&mut self, index: usize, value: &T) -> Result<(), Error> { + if index >= self.num_fields { + return Err(Error::buffer_out_of_bound(index, 1, self.num_fields)); + } + if value.is_null() { + let bitmap = + &mut self.writer.bf[self.base_offset..self.base_offset + self.bitmap_width]; + set_bit(bitmap, index); + return Ok(()); + } - pub fn write_end(&mut self, callback_info: WriteCallbackInfo) { - self.field_writer_helper.write_end(callback_info) + let slot_offset = self + .base_offset + .checked_add(self.bitmap_width) + .and_then(|offset| offset.checked_add(index * 8)) + .ok_or_else(|| Error::invalid_data("row field offset overflow"))?; + match T::FIXED_SIZE { + Some(width) => { + slot_width(Some(width))?; + let capacity = self.writer.bf.len(); + let output = self + .writer + .bf + .get_mut(slot_offset..slot_offset + width) + .ok_or_else(|| Error::buffer_out_of_bound(slot_offset, width, capacity))?; + value.write(ValueWriter::fixed(output)) + } + None => write_variable( + self.writer, + self.base_offset, + self.fixed_end, + slot_offset, + value, + ), + } } } -pub struct ArrayWriter<'a, 'b> { - field_writer_helper: FieldWriterHelper<'a, 'b>, +pub(crate) struct ArrayWriter<'a, 'b, T: RowValue> { + writer: &'a mut Writer<'b>, + base_offset: usize, + bitmap_width: usize, + header_size: usize, + num_elements: usize, + element_size: usize, + fixed_end: usize, + marker: PhantomData, } -impl<'a, 'b> ArrayWriter<'a, 'b> { - fn get_fixed_size(bit_map_width_in_bytes: usize, num_fields: usize) -> usize { - 8 + bit_map_width_in_bytes + num_fields * 8 - } - - pub fn new( - num_fields: usize, - writer: &'a mut Writer<'b>, - ) -> Result, Error> { +impl<'a, 'b, T: RowValue> ArrayWriter<'a, 'b, T> { + pub(crate) fn new(num_elements: usize, writer: &'a mut Writer<'b>) -> Result { + let count = u64::try_from(num_elements) + .map_err(|_| Error::invalid_data("row array element count exceeds u64"))?; let base_offset = writer.len(); - let bit_map_width_in_bytes = calculate_bitmap_width_in_bytes(num_fields); - let array_writer = ArrayWriter { - field_writer_helper: FieldWriterHelper::new( - writer, - base_offset, - Box::new(move |idx| 8 + base_offset + bit_map_width_in_bytes + idx * 8), - ), - }; - let fixed_size = Self::get_fixed_size(bit_map_width_in_bytes, num_fields); - array_writer.field_writer_helper.writer.reserve(fixed_size); - array_writer - .field_writer_helper - .writer - .write_u64(num_fields as u64); - array_writer.field_writer_helper.writer.skip(fixed_size - 8); - Ok(array_writer) - } + let bitmap_width = bitmap_width(num_elements)?; + let header_size = 8usize + .checked_add(bitmap_width) + .ok_or_else(|| Error::invalid_data("row array header size overflow"))?; + let element_size = slot_width(T::FIXED_SIZE)?; + let element_bytes = num_elements + .checked_mul(element_size) + .ok_or_else(|| Error::invalid_data("row array fixed region size overflow"))?; + let aligned_element_bytes = round_up_to_word(element_bytes)?; + let fixed_size = header_size + .checked_add(aligned_element_bytes) + .ok_or_else(|| Error::invalid_data("row array fixed region size overflow"))?; + let fixed_end = base_offset + .checked_add(fixed_size) + .ok_or_else(|| Error::invalid_data("row array fixed region size overflow"))?; - pub fn get_writer(&mut self) -> &mut Writer<'b> { - self.field_writer_helper.writer + writer.write_u64(count); + writer.skip(fixed_size - 8); + Ok(Self { + writer, + base_offset, + bitmap_width, + header_size, + num_elements, + element_size, + fixed_end, + marker: PhantomData, + }) } - pub fn write_start(&mut self, idx: usize) -> WriteCallbackInfo { - self.field_writer_helper.write_start(idx) - } + pub(crate) fn write(&mut self, index: usize, value: &T) -> Result<(), Error> { + if index >= self.num_elements { + return Err(Error::buffer_out_of_bound(index, 1, self.num_elements)); + } + if value.is_null() { + let bitmap_start = self.base_offset + 8; + let bitmap = &mut self.writer.bf[bitmap_start..bitmap_start + self.bitmap_width]; + set_bit(bitmap, index); + return Ok(()); + } - pub fn write_end(&mut self, callback_info: WriteCallbackInfo) { - self.field_writer_helper.write_end(callback_info) + let slot_offset = self + .base_offset + .checked_add(self.header_size) + .and_then(|offset| offset.checked_add(index * self.element_size)) + .ok_or_else(|| Error::invalid_data("row array element offset overflow"))?; + match T::FIXED_SIZE { + Some(width) => { + let capacity = self.writer.bf.len(); + let output = self + .writer + .bf + .get_mut(slot_offset..slot_offset + width) + .ok_or_else(|| Error::buffer_out_of_bound(slot_offset, width, capacity))?; + value.write(ValueWriter::fixed(output)) + } + None => write_variable( + self.writer, + self.base_offset, + self.fixed_end, + slot_offset, + value, + ), + } } } -pub struct MapWriter<'a, 'b> { - base_offset: usize, +pub(crate) struct MapWriter<'a, 'b> { writer: &'a mut Writer<'b>, + base_offset: usize, } impl<'a, 'b> MapWriter<'a, 'b> { - fn get_fixed_size(&self) -> usize { - // key_byte_size - 8 - } - - pub fn new(writer: &'a mut Writer<'b>) -> MapWriter<'a, 'b> { + pub(crate) fn new(writer: &'a mut Writer<'b>) -> Self { let base_offset = writer.len(); - let array_writer = MapWriter { + writer.skip(8); + Self { writer, base_offset, - }; - let fixed_size = array_writer.get_fixed_size(); - array_writer.writer.reserve(fixed_size); - array_writer.writer.skip(fixed_size); - array_writer + } } - pub fn get_writer(&mut self) -> &mut Writer<'b> { + pub(crate) fn write(&mut self, values: &BTreeMap) -> Result<(), Error> + where + K: RowValue + Ord, + V: RowValue, + { + let keys_start = self.writer.len(); + { + let mut keys = ArrayWriter::::new(values.len(), self.writer)?; + for (index, key) in values.keys().enumerate() { + keys.write(index, key)?; + } + } + let keys_size = self + .writer + .len() + .checked_sub(keys_start) + .ok_or_else(|| Error::invalid_data("row map key array size underflow"))?; + let keys_size = u64::try_from(keys_size) + .map_err(|_| Error::invalid_data("row map key array exceeds u64"))?; self.writer - } + .set_bytes(self.base_offset, &keys_size.to_le_bytes()); - pub fn write_start(&mut self, _idx: usize) -> usize { - self.writer.len() + let mut items = ArrayWriter::::new(values.len(), self.writer)?; + for (index, value) in values.values().enumerate() { + items.write(index, value)?; + } + Ok(()) } +} - pub fn write_end(&mut self, data_start: usize) { - let size: usize = self.writer.len() - data_start; - self.writer - .set_bytes(self.base_offset, &(size as u64).to_le_bytes()); +fn write_variable( + writer: &mut Writer<'_>, + base_offset: usize, + fixed_end: usize, + slot_offset: usize, + value: &T, +) -> Result<(), Error> { + let data_start = writer.len(); + if data_start < fixed_end { + return Err(Error::invalid_data( + "row variable data overlaps the fixed region", + )); } + let relative_offset = data_start + .checked_sub(base_offset) + .ok_or_else(|| Error::invalid_data("row variable offset underflow"))?; + let relative_offset = u32::try_from(relative_offset) + .map_err(|_| Error::invalid_data("row variable offset exceeds u32"))?; + + value.write(ValueWriter::variable(writer))?; + let size = writer + .len() + .checked_sub(data_start) + .ok_or_else(|| Error::invalid_data("row variable size underflow"))?; + let wire_size = + u32::try_from(size).map_err(|_| Error::invalid_data("row variable size exceeds u32"))?; + let aligned_size = round_up_to_word(size)?; + writer.skip(aligned_size - size); + + let offset_and_size = ((relative_offset as u64) << 32) | wire_size as u64; + writer.set_bytes(slot_offset, &offset_and_size.to_le_bytes()); + Ok(()) } -pub fn to_row<'a, T: Row<'a>>(v: &T) -> Result, Error> { - let mut buffer = vec![]; +/// Encodes a struct, array, or map as a Standard Row Format root. +pub fn to_row(value: &T) -> Result, Error> { + let mut buffer = Vec::new(); let mut writer = Writer::from_buffer(&mut buffer); - T::write(v, &mut writer)?; + value.write(ValueWriter::variable(&mut writer))?; Ok(buffer) } diff --git a/rust/fory-derive/Cargo.toml b/rust/fory-derive/Cargo.toml index 0fa4ad8c74..fbf3643aec 100644 --- a/rust/fory-derive/Cargo.toml +++ b/rust/fory-derive/Cargo.toml @@ -39,6 +39,7 @@ syn = { default-features = false, version = "2.0", features = [ "parsing", "proc-macro", "derive", + "fold", "printing", ] } quote = { default-features = false, version = "1.0" } diff --git a/rust/fory-derive/src/fory_row.rs b/rust/fory-derive/src/fory_row.rs index 9b691b03d5..f7145d2fbe 100644 --- a/rust/fory-derive/src/fory_row.rs +++ b/rust/fory-derive/src/fory_row.rs @@ -15,71 +15,201 @@ // specific language governing permissions and limitations // under the License. -use crate::util::{extract_fields, source_fields}; use proc_macro::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; +use syn::fold::{self, Fold}; +use syn::{Data, DeriveInput, Fields, GenericParam, Lifetime, LifetimeParam, Path}; -pub fn derive_row(ast: &syn::DeriveInput, runtime_root: proc_macro2::TokenStream) -> TokenStream { - let name = &ast.ident; - let source_fields = match &ast.data { - syn::Data::Struct(s) => source_fields(&s.fields), - _ => { - panic!("only struct be supported") +pub fn derive_row(ast: &DeriveInput, runtime_root: proc_macro2::TokenStream) -> TokenStream { + match expand_row(ast, runtime_root) { + Ok(tokens) => tokens.into(), + Err(error) => error.into_compile_error().into(), + } +} + +fn expand_row( + ast: &DeriveInput, + runtime_root: proc_macro2::TokenStream, +) -> syn::Result { + let fields = match &ast.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + Fields::Unnamed(fields) => { + return Err(syn::Error::new_spanned( + fields, + "ForyRow can only be derived for structs with named fields", + )); + } + Fields::Unit => { + return Err(syn::Error::new_spanned( + ast, + "ForyRow can only be derived for structs with named fields", + )); + } + }, + Data::Enum(_) => { + return Err(syn::Error::new_spanned( + ast, + "ForyRow cannot be derived for enums", + )); + } + Data::Union(_) => { + return Err(syn::Error::new_spanned( + ast, + "ForyRow cannot be derived for unions", + )); } }; - let fields = extract_fields(&source_fields); - let write_exprs = fields.iter().enumerate().map(|(index, field)| { + let name = &ast.ident; + let visibility = &ast.vis; + let view = format_ident!("{}RowView", name); + let view_doc = format!("A zero-copy Standard Row Format view of `{name}`."); + let num_fields = fields.len(); + let row_lifetime = row_lifetime(ast); + + let mut row_generics = ast.generics.clone(); + for field in fields { let ty = &field.ty; - let ident = field.ident.as_ref().expect("field should provide ident"); + let predicate = syn::parse2(quote! { + #ty: #runtime_root::row::RowValue + })?; + row_generics.make_where_clause().predicates.push(predicate); + } - quote! { - let mut callback_info = struct_writer.write_start(#index); - <#ty as #runtime_root::row::Row<'a>>::write(&v.#ident, struct_writer.get_writer())?; - struct_writer.write_end(callback_info); - } - }); + let source_path: Path = { + let (_, source_ty_generics, _) = row_generics.split_for_impl(); + syn::parse2(quote! { #name #source_ty_generics })? + }; + // Copied bounds and field types live on the generated view, where an + // unmodified `Self` would refer to the view instead of the source row. + let mut view_generics = SelfTypeRewriter { + source_path: source_path.clone(), + } + .fold_generics(row_generics.clone()); + view_generics.params.insert( + 0, + GenericParam::Lifetime(LifetimeParam::new(row_lifetime.clone())), + ); + let (impl_generics, ty_generics, where_clause) = row_generics.split_for_impl(); + let (view_impl_generics, view_ty_generics, view_where_clause) = view_generics.split_for_impl(); - let getter_exprs = fields.iter().enumerate().map(|(index, field)| { + let mut writes = Vec::with_capacity(num_fields); + let mut field_methods = Vec::with_capacity(num_fields); + for (index, field) in fields.iter().enumerate() { + let ident = field.ident.as_ref().ok_or_else(|| { + syn::Error::new_spanned(field, "ForyRow requires named struct fields") + })?; + let field_visibility = &field.vis; + let field_doc = format!("Reads the `{ident}` field from this row view."); let ty = &field.ty; - let ident = field.ident.as_ref().expect("field should provide ident"); - let getter_name: proc_macro2::Ident = syn::Ident::new(&format!("{ident}"), ident.span()); - - quote! { - pub fn #getter_name(&self) -> <#ty as #runtime_root::row::Row<'a>>::ReadResult { - let bytes = self.struct_data.get_field_bytes(#index); - <#ty as #runtime_root::row::Row<'a>>::cast(bytes) + let field_ty = SelfTypeRewriter { + source_path: source_path.clone(), + } + .fold_type(ty.clone()); + writes.push(quote! { + struct_writer.write(#index, &self.#ident)?; + }); + field_methods.push(quote! { + #[doc = #field_doc] + #[inline] + #field_visibility fn #ident( + &self, + ) -> ::core::result::Result< + <#field_ty as #runtime_root::row::RowValue>::View<#row_lifetime>, + #runtime_root::error::Error, + > { + self.struct_data.get::<#field_ty>(#index) } + }); + } + + Ok(quote! { + #[doc = #view_doc] + #visibility struct #view #view_impl_generics #view_where_clause { + struct_data: #runtime_root::row::StructView<#row_lifetime>, + _marker: ::core::marker::PhantomData *const #name #ty_generics>, + } + + impl #view_impl_generics #view #view_ty_generics #view_where_clause { + #(#field_methods)* } - }); - let getter: proc_macro2::Ident = syn::Ident::new(&format!("{name}ForyRowGetter"), name.span()); + impl #impl_generics #runtime_root::row::RowValue for #name #ty_generics #where_clause { + type View<#row_lifetime> = #view #view_ty_generics; - let num_fields = fields.len(); + const FIXED_SIZE: ::core::option::Option = ::core::option::Option::None; + + fn write( + &self, + writer: #runtime_root::row::ValueWriter<'_, '_>, + ) -> ::core::result::Result<(), #runtime_root::error::Error> { + let mut struct_writer = writer.struct_writer(#num_fields)?; + #(#writes)* + ::core::result::Result::Ok(()) + } - let gen = quote! { - struct #getter<'a> { - struct_data: #runtime_root::row::StructViewer<'a> + fn read<#row_lifetime>( + bytes: &#row_lifetime [u8], + ) -> ::core::result::Result, #runtime_root::error::Error> { + ::core::result::Result::Ok(#view { + struct_data: #runtime_root::row::StructView::new(bytes, #num_fields)?, + _marker: ::core::marker::PhantomData, + }) + } } - impl<'a> #getter<'a> { - #(#getter_exprs)* + impl #impl_generics #runtime_root::row::Row for #name #ty_generics #where_clause {} + }) +} + +fn row_lifetime(ast: &DeriveInput) -> Lifetime { + let mut collector = LifetimeCollector::default(); + collector.fold_derive_input(ast.clone()); + let mut suffix = 0usize; + loop { + let name = if suffix == 0 { + "__fory_row".to_owned() + } else { + format!("__fory_row_{suffix}") + }; + if !collector.names.iter().any(|used| used == &name) { + return Lifetime::new(&format!("'{name}"), proc_macro2::Span::call_site()); } + suffix += 1; + } +} - impl<'a> #runtime_root::row::Row<'a> for #name { +#[derive(Default)] +struct LifetimeCollector { + names: Vec, +} - type ReadResult = #getter<'a>; +impl Fold for LifetimeCollector { + fn fold_lifetime(&mut self, lifetime: Lifetime) -> Lifetime { + self.names.push(lifetime.ident.to_string()); + lifetime + } +} - fn write(v: &Self, writer: &mut #runtime_root::buffer::Writer) -> Result<(), #runtime_root::error::Error> { - let mut struct_writer = #runtime_root::row::StructWriter::new(#num_fields, writer); - #(#write_exprs);*; - Ok(()) - } +struct SelfTypeRewriter { + source_path: Path, +} - fn cast(bytes: &'a [u8]) -> Self::ReadResult { - #getter{ struct_data: #runtime_root::row::StructViewer::new(bytes, #num_fields) } - } +impl Fold for SelfTypeRewriter { + fn fold_path(&mut self, path: Path) -> Path { + let mut path = fold::fold_path(self, path); + let Some(first) = path.segments.first() else { + return path; + }; + if path.leading_colon.is_some() || first.ident != "Self" { + return path; } - }; - gen.into() + + let mut segments = self.source_path.segments.clone(); + segments.extend(path.segments.into_iter().skip(1)); + path.leading_colon = self.source_path.leading_colon; + path.segments = segments; + path + } } diff --git a/rust/fory-derive/src/lib.rs b/rust/fory-derive/src/lib.rs index d0749dbd65..d30efe1992 100644 --- a/rust/fory-derive/src/lib.rs +++ b/rust/fory-derive/src/lib.rs @@ -67,27 +67,43 @@ //! //! ### `#[derive(ForyRow)]` //! -//! Generates row-based serialization code for structs. This macro implements -//! the `Row` trait, enabling zero-copy deserialization for maximum performance. +//! Generates Standard Row Format serialization and borrowed field views for a +//! named struct. The macro implements `RowValue` and the root `Row` marker. +//! Enums, unions, tuple structs, and unit structs are rejected at compile time. //! //! **Supported Types:** -//! - Structs with named fields only -//! - All field types must implement the `Row` trait +//! - Fixed values: `bool`, `i8`, `i16`, `i32`, `i64`, `f32`, `f64`, `Date`, +//! `Timestamp`, and `Duration` +//! - Variable values: `String` and `&str`, binary `Vec` and `&[u8]`, fixed +//! and variable arrays, `BTreeMap`, and other derived row structs +//! - `Option` for nullable fields and array elements +//! - Every field type must implement `RowValue` //! //! **Example:** //! ```rust +//! use fory_core::error::Error; +//! use fory_core::row::{from_row, to_row}; //! use fory_derive::ForyRow; -//! use std::collections::BTreeMap; //! //! #[derive(ForyRow)] //! struct UserProfile { //! id: i64, //! username: String, -//! email: String, -//! scores: Vec, -//! preferences: BTreeMap, -//! is_active: bool, +//! email: Option, //! } +//! +//! # fn main() -> Result<(), Error> { +//! let bytes = to_row(&UserProfile { +//! id: 7, +//! username: "fory".to_owned(), +//! email: None, +//! })?; +//! let view = from_row::(&bytes)?; +//! assert_eq!(view.id()?, 7); +//! assert_eq!(view.username()?, "fory"); +//! assert_eq!(view.email()?, None); +//! # Ok(()) +//! # } //! ``` //! //! ## Generated Code @@ -103,10 +119,10 @@ //! ### For `#[derive(ForyRow)]` //! //! The macro generates: -//! - `Row` trait implementation -//! - A getter struct for zero-copy field access -//! - Field accessor methods that return references to the underlying data -//! - Efficient serialization without object allocation +//! - A `RowValue` implementation and a root `Row` marker implementation +//! - A borrowed view type whose visibility matches the source struct +//! - One declaration-order field method preserving each source field's visibility +//! - Field methods returning `Result<::View<'_>, Error>` //! //! ## Attributes //! @@ -131,11 +147,11 @@ //! //! ## Field Types //! -//! Both macros support a wide range of field types: +//! The object-format derives support a wide range of field types: //! //! **Primitive Types:** //! - `bool`, `i8`, `i16`, `i32`, `i64`, `f32`, `f64` -//! - `String`, `&str` (in row format) +//! - `String` //! - `Vec` for binary data //! //! **Collections:** @@ -150,7 +166,11 @@ //! - `chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration` when the `chrono` feature is enabled //! //! **Custom Types:** -//! - Any type that implements `Serializer` (for `Fory`) or `Row` (for `ForyRow`) +//! - Any type that implements `Serializer` +//! +//! `ForyRow` uses the separate, exact type set documented under its macro +//! section. A row field implements `RowValue`; only derived structs, arrays, +//! and maps implement the root `Row` marker. //! //! Derived structs, enums, and unions can be used behind //! `Arc` when the concrete type satisfies `Send + Sync`. @@ -192,9 +212,8 @@ //! ## Performance Considerations //! //! - **`Fory`**: Best for complex object graphs with references and nested structures -//! - **`ForyRow`**: Best for high-throughput scenarios requiring zero-copy access +//! - **`ForyRow`**: Provides lazy, borrowed access to Standard Row Format data //! - Both macros generate optimized code with minimal runtime overhead -//! - Field access in row format is extremely fast as it involves no allocations use fory_row::derive_row; use proc_macro::TokenStream; @@ -273,24 +292,36 @@ fn derive_serializer(input: DeriveInput) -> TokenStream { object::derive_serializer(&input, attrs, runtime_root) } -/// Derive macro for row-based serialization. +/// Derive macro for Standard Row Format serialization. /// -/// This macro generates code to implement the `Row` trait for the annotated -/// type, enabling zero-copy deserialization for maximum performance in -/// high-throughput scenarios. +/// This macro accepts named structs whose fields implement `RowValue`. It +/// implements `RowValue` and the root `Row` marker, preserves field declaration +/// order, and generates a borrowed view with field methods that return `Result`. /// /// # Example /// /// ```rust +/// use fory_core::error::Error; +/// use fory_core::row::{from_row, to_row}; /// use fory_derive::ForyRow; /// /// #[derive(ForyRow)] /// struct UserProfile { /// id: i64, /// username: String, -/// email: String, -/// is_active: bool, +/// email: Option, /// } +/// +/// # fn main() -> Result<(), Error> { +/// let bytes = to_row(&UserProfile { +/// id: 7, +/// username: "fory".to_owned(), +/// email: None, +/// })?; +/// let view = from_row::(&bytes)?; +/// assert_eq!(view.username()?, "fory"); +/// # Ok(()) +/// # } /// ``` #[proc_macro_derive(ForyRow)] pub fn proc_macro_derive_fory_row(input: proc_macro::TokenStream) -> TokenStream { diff --git a/rust/fory/src/lib.rs b/rust/fory/src/lib.rs index 92c70870ec..aa7914870c 100644 --- a/rust/fory/src/lib.rs +++ b/rust/fory/src/lib.rs @@ -88,7 +88,11 @@ extern crate self as fory; pub use fory_core::{ - error::Error, fory::Fory, fory::ForyBuilder, register_trait_type, row::from_row, row::to_row, + error::Error, + fory::Fory, + fory::ForyBuilder, + register_trait_type, + row::{from_row, to_row, ArrayView, MapView, Row}, ArcSerializer, ArcWeak, ArcWeakSerializer, ArraySerializer, BFloat16, BTreeMapSerializer, BTreeSetSerializer, BinaryHeapSerializer, BoxSerializer, Date, Decimal, Duration, Float16, ForyObject, HashMapSerializer, HashSetSerializer, LinkedListSerializer, MutexSerializer, diff --git a/rust/tests/tests/test_row.rs b/rust/tests/tests/test_row.rs index 488a57cfb9..31cec0e04b 100644 --- a/rust/tests/tests/test_row.rs +++ b/rust/tests/tests/test_row.rs @@ -18,124 +18,353 @@ use std::collections::BTreeMap; use fory_core::row::{from_row, to_row}; +use fory_core::types::{Date, Duration, Timestamp}; use fory_derive::ForyRow; -#[test] -fn row_with_array_field() { - // Test from GitHub issue: ForyRow should work with fixed-size array fields - #[derive(ForyRow)] - struct PointWithArray { - index: i32, - point: [f32; 4], +#[derive(ForyRow)] +struct MixedRow { + number: i32, + text: String, + short: i16, +} + +#[derive(ForyRow)] +struct NestedChild { + value: i32, +} + +#[derive(ForyRow)] +struct NestedParent { + child: NestedChild, +} + +#[derive(ForyRow)] +struct CollectionRow { + values: Vec, + mapping: BTreeMap, +} + +#[derive(ForyRow)] +struct NullableRow { + empty: String, + missing: Option, + number: Option, +} + +#[derive(ForyRow)] +struct TemporalRow { + date: Date, + timestamp: Timestamp, + duration: Duration, +} + +#[derive(ForyRow)] +struct GenericRow<'__fory_row, T, const N: usize> +where + Self: '__fory_row, + T: Copy, +{ + label: &'__fory_row str, + value: T, + values: [T; N], +} + +#[derive(ForyRow)] +struct AssociatedRow<'source> +where + Self: std::ops::Deref, +{ + value: &'source ::Target, +} + +impl std::ops::Deref for AssociatedRow<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.value } +} - let data = PointWithArray { - index: 42, - point: [1.0, 2.0, 3.0, 4.0], - }; +trait RowBorrow<'row> {} - let row = to_row(&data).unwrap(); - let obj = from_row::(&row); +impl<'row> RowBorrow<'row> for i32 {} - assert_eq!(obj.index(), 42); - let point_getter = obj.point(); - assert_eq!(point_getter.size(), 4); - assert_eq!(point_getter.get(0).expect("index 0"), 1.0); - assert_eq!(point_getter.get(1).expect("index 1"), 2.0); - assert_eq!(point_getter.get(2).expect("index 2"), 3.0); - assert_eq!(point_getter.get(3).expect("index 3"), 4.0); - assert!(point_getter.get(4).is_err()); +#[derive(ForyRow)] +struct HrtbRow +where + for<'__fory_row> T: RowBorrow<'__fory_row>, +{ + value: T, } #[test] -fn row_with_nested_struct_array() { - // Test ForyRow with nested struct containing arrays - #[derive(ForyRow)] - struct Point3D { - coords: [f64; 3], - } +fn standard_row_bytes() { + let bytes = to_row(&MixedRow { + number: 0x1234_5678, + text: "A".to_owned(), + short: 0x1234, + }) + .unwrap(); - #[derive(ForyRow)] - struct Geometry { - name: String, - origin: Point3D, - } + // This is the raw Standard Row Format vector emitted by the Java/C++ + // standard writers for schema [int32, utf8, int16]. + let expected = [ + 0, 0, 0, 0, 0, 0, 0, 0, // null bitmap + 0x78, 0x56, 0x34, 0x12, 0, 0, 0, 0, // inline int32 slot + 1, 0, 0, 0, 32, 0, 0, 0, // string size, then relative offset + 0x34, 0x12, 0, 0, 0, 0, 0, 0, // inline int16 slot + b'A', 0, 0, 0, 0, 0, 0, 0, // string and zero padding + ]; + assert_eq!(bytes, expected); - let data = Geometry { - name: String::from("origin"), - origin: Point3D { - coords: [0.0, 0.0, 0.0], - }, - }; + let view = from_row::(&bytes).unwrap(); + assert_eq!(view.number().unwrap(), 0x1234_5678); + assert_eq!(view.text().unwrap(), "A"); + assert_eq!(view.short().unwrap(), 0x1234); +} + +#[test] +fn generic_row() { + let bytes = to_row(&GenericRow { + label: "generic", + value: 7i32, + values: [1, 2, 3], + }) + .unwrap(); + + let view = from_row::>(&bytes).unwrap(); + assert_eq!(view.label().unwrap(), "generic"); + assert_eq!(view.value().unwrap(), 7); + let values = view.values().unwrap(); + assert_eq!(values.len(), 3); + assert_eq!(values.get(0).unwrap(), 1); + assert_eq!(values.get(2).unwrap(), 3); + + let associated = to_row(&AssociatedRow { + value: "associated", + }) + .unwrap(); + let associated_view = from_row::>(&associated).unwrap(); + assert_eq!(associated_view.value().unwrap(), "associated"); + + let hrtb = to_row(&HrtbRow { value: 9i32 }).unwrap(); + let hrtb_view = from_row::>(&hrtb).unwrap(); + assert_eq!(hrtb_view.value().unwrap(), 9); +} + +#[test] +fn primitive_array_bytes() { + let bytes = to_row(&vec![0x1234_5678i32]).unwrap(); + let expected = [ + 1, 0, 0, 0, 0, 0, 0, 0, // element count + 0, 0, 0, 0, 0, 0, 0, 0, // null bitmap + 0x78, 0x56, 0x34, 0x12, 0, 0, 0, 0, // natural-width value and padding + ]; + assert_eq!(bytes, expected); + + let view = from_row::>(&bytes).unwrap(); + assert_eq!(view.len(), 1); + assert_eq!(view.get(0).unwrap(), 0x1234_5678); + assert!(view.get(1).is_err()); + + let boolean = to_row(&vec![true]).unwrap(); + assert_eq!(boolean.len(), 24); + assert_eq!(&boolean[16..], &[1, 0, 0, 0, 0, 0, 0, 0]); + + let int8 = to_row(&vec![-1i8]).unwrap(); + assert_eq!(&int8[16..], &[0xff, 0, 0, 0, 0, 0, 0, 0]); + + let int16 = to_row(&vec![0x1234i16]).unwrap(); + assert_eq!(&int16[16..], &[0x34, 0x12, 0, 0, 0, 0, 0, 0]); - let row = to_row(&data).unwrap(); - let obj = from_row::(&row); + let int64 = to_row(&vec![0x0102_0304_0506_0708i64]).unwrap(); + assert_eq!( + &int64[16..], + &[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01] + ); - assert_eq!(obj.name(), "origin"); - let coords = obj.origin().coords(); - assert_eq!(coords.size(), 3); - assert_eq!(coords.get(0).expect("index 0"), 0.0); - assert_eq!(coords.get(1).expect("index 1"), 0.0); - assert_eq!(coords.get(2).expect("index 2"), 0.0); - assert!(coords.get(3).is_err()); + let float32 = to_row(&vec![1.0f32]).unwrap(); + assert_eq!(&float32[16..], &[0, 0, 0x80, 0x3f, 0, 0, 0, 0]); + + let float64 = to_row(&vec![1.0f64]).unwrap(); + assert_eq!(&float64[16..], &[0, 0, 0, 0, 0, 0, 0xf0, 0x3f]); +} + +#[test] +fn nullable_variable_array_bytes() { + let values = vec![Some("a".to_owned()), None, Some("bc".to_owned())]; + let bytes = to_row(&values).unwrap(); + let expected = [ + 3, 0, 0, 0, 0, 0, 0, 0, // element count + 2, 0, 0, 0, 0, 0, 0, 0, // element 1 is null + 1, 0, 0, 0, 40, 0, 0, 0, // "a" + 0, 0, 0, 0, 0, 0, 0, 0, // null slot remains zero + 2, 0, 0, 0, 48, 0, 0, 0, // "bc" + b'a', 0, 0, 0, 0, 0, 0, 0, // first body + b'b', b'c', 0, 0, 0, 0, 0, 0, // second body + ]; + assert_eq!(bytes, expected); + + let view = from_row::>>(&bytes).unwrap(); + assert_eq!(view.get(0).unwrap(), Some("a")); + assert_eq!(view.get(1).unwrap(), None); + assert_eq!(view.get(2).unwrap(), Some("bc")); +} + +#[test] +fn standard_map_bytes() { + let values = BTreeMap::from([("k".to_owned(), 7i32)]); + let bytes = to_row(&values).unwrap(); + let expected = [ + 32, 0, 0, 0, 0, 0, 0, 0, // key array byte size + 1, 0, 0, 0, 0, 0, 0, 0, // key count + 0, 0, 0, 0, 0, 0, 0, 0, // key bitmap + 1, 0, 0, 0, 24, 0, 0, 0, // key offset-size + b'k', 0, 0, 0, 0, 0, 0, 0, // key body + 1, 0, 0, 0, 0, 0, 0, 0, // value count + 0, 0, 0, 0, 0, 0, 0, 0, // value bitmap + 7, 0, 0, 0, 0, 0, 0, 0, // value and padding + ]; + assert_eq!(bytes, expected); + + let view = from_row::>(&bytes).unwrap(); + assert_eq!(view.keys().get(0).unwrap(), "k"); + assert_eq!(view.values().get(0).unwrap(), 7); + assert_eq!(view.to_btree_map().unwrap(), BTreeMap::from([("k", 7)])); +} + +#[test] +fn nested_row_bytes() { + let bytes = to_row(&NestedParent { + child: NestedChild { value: 7 }, + }) + .unwrap(); + let expected = [ + 0, 0, 0, 0, 0, 0, 0, 0, // parent bitmap + 16, 0, 0, 0, 16, 0, 0, 0, // child size and parent-relative offset + 0, 0, 0, 0, 0, 0, 0, 0, // child bitmap + 7, 0, 0, 0, 0, 0, 0, 0, // child field slot + ]; + assert_eq!(bytes, expected); + + let view: NestedParentRowView<'_> = from_row::(&bytes).unwrap(); + assert_eq!(view.child().unwrap().value().unwrap(), 7); +} + +#[test] +fn nested_container_offsets() { + let bytes = to_row(&CollectionRow { + values: vec!["a".to_owned()], + mapping: BTreeMap::from([("k".to_owned(), "v".to_owned())]), + }) + .unwrap(); + + assert_eq!(&bytes[8..16], &[32, 0, 0, 0, 24, 0, 0, 0]); + assert_eq!(&bytes[16..24], &[72, 0, 0, 0, 56, 0, 0, 0]); + assert_eq!(&bytes[40..48], &[1, 0, 0, 0, 24, 0, 0, 0]); + assert_eq!(&bytes[80..88], &[1, 0, 0, 0, 24, 0, 0, 0]); + assert_eq!(&bytes[112..120], &[1, 0, 0, 0, 24, 0, 0, 0]); + + let view = from_row::(&bytes).unwrap(); + assert_eq!(view.values().unwrap().get(0).unwrap(), "a"); + assert_eq!( + view.mapping().unwrap().to_btree_map().unwrap(), + BTreeMap::from([("k", "v")]) + ); } #[test] -fn row() { - #[derive(ForyRow)] - struct Foo { - f1: String, - f2: i8, - f3: Vec, - f4: Vec, - f5: BTreeMap, +fn null_and_empty_are_distinct() { + let bytes = to_row(&NullableRow { + empty: String::new(), + missing: None, + number: None, + }) + .unwrap(); + assert_eq!(bytes.len(), 32); + assert_eq!(bytes[0], 0b0000_0110); + assert_eq!(&bytes[8..16], &[0, 0, 0, 0, 32, 0, 0, 0]); + assert_eq!(&bytes[16..32], &[0; 16]); + + let view = from_row::(&bytes).unwrap(); + assert_eq!(view.empty().unwrap(), ""); + assert_eq!(view.missing().unwrap(), None); + assert_eq!(view.number().unwrap(), None); +} + +#[test] +fn null_bitmap_crosses_words() { + let mut values = vec![Some(1i8); 65]; + for index in [0, 7, 8, 63, 64] { + values[index] = None; } + let bytes = to_row(&values).unwrap(); + assert_eq!( + &bytes[8..24], + &[0x81, 0x01, 0, 0, 0, 0, 0, 0x80, 1, 0, 0, 0, 0, 0, 0, 0] + ); - #[derive(ForyRow)] - struct Bar { - f3: Foo, + let view = from_row::>>(&bytes).unwrap(); + for index in 0..65 { + let expected = if [0, 7, 8, 63, 64].contains(&index) { + None + } else { + Some(1) + }; + assert_eq!(view.get(index).unwrap(), expected); } +} + +#[test] +fn temporal_slots() { + let value = TemporalRow { + date: Date::from_epoch_days(-2), + timestamp: Timestamp::from_epoch_micros(-1), + duration: Duration::from_micros(1_500_000), + }; + let bytes = to_row(&value).unwrap(); + assert_eq!(&bytes[8..12], &(-2i32).to_le_bytes()); + assert_eq!(&bytes[16..24], &(-1i64).to_le_bytes()); + assert_eq!(&bytes[24..32], &(1_500_000i64).to_le_bytes()); - let mut f5: BTreeMap = BTreeMap::new(); - f5.insert(String::from("k1"), String::from("v1")); - f5.insert(String::from("k2"), String::from("v2")); - - let row = to_row(&Bar { - f3: Foo { - f1: String::from("hello"), - f2: 1, - f3: vec![1, 2, 3], - f4: vec![-1, 2, -3], - f5, - }, + let view = from_row::(&bytes).unwrap(); + assert_eq!(view.date().unwrap(), value.date); + assert_eq!(view.timestamp().unwrap(), value.timestamp); + assert_eq!(view.duration().unwrap(), value.duration); +} + +#[test] +fn malformed_rows_return_errors() { + let valid = to_row(&MixedRow { + number: 1, + text: "A".to_owned(), + short: 2, }) .unwrap(); + assert!(from_row::(&valid[..31]).is_err()); + + let mut overlap = valid.clone(); + overlap[16..24].copy_from_slice(&1u64.to_le_bytes()); + assert!(from_row::(&overlap).unwrap().text().is_err()); + + let mut outside = valid.clone(); + outside[16..24].copy_from_slice(&(((32u64) << 32) | 100).to_le_bytes()); + assert!(from_row::(&outside).unwrap().text().is_err()); + + let mut invalid_utf8 = valid; + invalid_utf8[32] = 0xff; + assert!(from_row::(&invalid_utf8).unwrap().text().is_err()); + + assert!(from_row::>(&u64::MAX.to_le_bytes()).is_err()); +} + +#[test] +fn container_shape_is_validated() { + let one = to_row(&vec![1i32]).unwrap(); + assert!(from_row::<[i32; 2]>(&one).is_err()); - let obj = from_row::(&row); - let f1: &str = obj.f3().f1(); - assert_eq!(f1, "hello"); - let f2: i8 = obj.f3().f2(); - assert_eq!(f2, 1); - let f3: &[u8] = obj.f3().f3(); - assert_eq!(f3, vec![1, 2, 3]); - let f4_size: usize = obj.f3().f4().size(); - assert_eq!(f4_size, 3); - assert_eq!(obj.f3().f4().get(0).expect("index 0"), -1); - assert_eq!(obj.f3().f4().get(1).expect("index 1"), 2); - assert_eq!(obj.f3().f4().get(2).expect("index 2"), -3); - assert!(obj.f3().f4().get(3).is_err()); - - let binding = obj.f3().f5(); - - assert_eq!(binding.keys().size(), 2); - assert_eq!(binding.keys().get(0).expect("key 0"), "k1"); - assert!(binding.keys().get(2).is_err()); - - assert_eq!(binding.values().size(), 2); - assert_eq!(binding.values().get(0).expect("value 0"), "v1"); - assert!(binding.values().get(2).is_err()); - - let f5 = binding.to_btree_map().expect("should be map"); - assert_eq!(f5.get("k1").expect("should exists"), &"v1"); - assert_eq!(f5.get("k2").expect("should exists"), &"v2"); + let map = to_row(&BTreeMap::from([("k".to_owned(), 7i32)])).unwrap(); + let mut mismatched = map; + mismatched[40..48].copy_from_slice(&2u64.to_le_bytes()); + assert!(from_row::>(&mismatched).is_err()); }