| --- |
| title: Xlang Implementation Guide |
| sidebar_position: 10 |
| id: xlang_implementation_guide |
| license: | |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| --- |
| |
| ## Overview |
| |
| This guide describes the current xlang runtime ownership model used by the |
| reference Java runtime and mirrored by the Dart runtime rewrite. |
| |
| The wire format is defined by |
| [Xlang Serialization Spec](xlang_serialization_spec.md). This document is about |
| service boundaries, operation flow, and internal ownership. New runtimes do not |
| need the same class names, but they should preserve the same control flow: |
| |
| - root operations stay on the runtime facade |
| - nested payload work stays on explicit read and write contexts |
| - type metadata stays in the type resolver layer |
| - serializers stay payload-focused |
| |
| When this guide conflicts with the wire-format specification, follow |
| `docs/specification/xlang_serialization_spec.md`. When it conflicts with a |
| runtime-specific implementation detail, follow the current runtime code for |
| that language. |
| |
| ## Source Of Truth |
| |
| Use these sources in this order: |
| |
| 1. `docs/specification/xlang_serialization_spec.md` |
| 2. the current runtime implementation for the language |
| 3. cross-language tests under `integration_tests/` |
| |
| For Dart, the runtime shape is centered on: |
| |
| - `Fory` |
| - `WriteContext` |
| - `ReadContext` |
| - `RefWriter` |
| - `RefReader` |
| - `TypeResolver` |
| - `StructCodec` |
| |
| ## Runtime Ownership Model |
| |
| ### `Fory` is the root-operation facade |
| |
| `Fory` owns the reusable runtime services for one runtime instance. |
| |
| In Dart, `Fory` owns exactly four runtime members: |
| |
| - `Buffer` |
| - `WriteContext` |
| - `ReadContext` |
| - `TypeResolver` |
| |
| In Java, `Fory` also owns runtime-local services such as `JITContext` and |
| `CopyContext`, but the ownership rule is the same: `Fory` is the root facade, |
| not the place where nested serializers do their work. |
| |
| `Fory` is responsible for: |
| |
| - preparing the shared buffer for root operations |
| - writing and reading the root xlang header bitmap |
| - delegating nested value encoding to `WriteContext` |
| - delegating nested value decoding to `ReadContext` |
| - owning registration through `TypeResolver` |
| - resetting operation-local context state in a top-level `finally` |
| |
| Nested serializers must not call back into root `serialize(...)` or |
| `deserialize(...)` entry points. |
| |
| ### `WriteContext` and `ReadContext` hold operation-local state |
| |
| `WriteContext` and `ReadContext` are prepared by `Fory` for one root operation |
| and reset by `Fory` in a `finally` block before reuse. |
| |
| `prepare(...)` should only bind the active buffer and root-operation inputs. |
| `reset()` should clear operation-local mutable state. |
| |
| That operation-local state includes: |
| |
| - the current buffer |
| - the active `RefWriter` or `RefReader` |
| - meta-string state |
| - shared type-definition state |
| - operation-local scratch state keyed by identity |
| - compatible struct slot state |
| - logical object-graph depth |
| |
| Generated and hand-written serializers should treat these contexts as the only |
| source of operation-local services. Serializers must not keep ambient runtime |
| state in thread locals, globals, or serializer instance fields. |
| |
| ### `WriteContext` |
| |
| `WriteContext` owns all write-side per-operation state: |
| |
| - current `Buffer` |
| - `RefWriter` |
| - `MetaStringWriter` |
| - shared TypeDef write state |
| - root `trackRef` mode |
| - recursion depth and limits |
| - local struct slot state used by compatible writes |
| |
| It exposes one-shot primitive helpers such as: |
| |
| - `writeBool` |
| - `writeInt32` |
| - `writeVarUint32` |
| |
| These helpers are convenience methods. Serializers that perform repeated |
| primitive IO should cache `final buffer = context.buffer;` and call buffer |
| methods directly. |
| |
| ### `ReadContext` |
| |
| `ReadContext` owns all read-side per-operation state: |
| |
| - current `Buffer` |
| - `RefReader` |
| - `MetaStringReader` |
| - shared TypeDef read state |
| - recursion depth and limits |
| - local struct slot state used by compatible reads |
| |
| It exposes matching one-shot primitive helpers such as: |
| |
| - `readBool` |
| - `readInt32` |
| - `readVarUint32` |
| |
| Generated struct serializers call `context.reference(value)` immediately after |
| constructing the target instance so back-references can resolve to that object. |
| |
| ## Reference Tracking |
| |
| Reference handling is split behind two explicit services: |
| |
| - `RefWriter` writes null, ref, and new-value markers and remembers previously |
| written objects by identity. |
| - `RefReader` decodes those markers, reserves read reference IDs, and resolves |
| previously materialized objects. |
| |
| The xlang ref markers are: |
| |
| - `NULL_FLAG (-3)` |
| - `REF_FLAG (-2)` |
| - `NOT_NULL_VALUE_FLAG (-1)` |
| - `REF_VALUE_FLAG (0)` |
| |
| Key behavior: |
| |
| - basic values never use ref tracking |
| - field metadata controls ref behavior inside generated structs |
| - root `trackRef` is only for top-level graphs and container roots with no |
| field metadata |
| - serializers that allocate an object before all nested reads complete must bind |
| that object early with `context.reference(...)` |
| |
| ## Type Resolution |
| |
| `TypeResolver` owns: |
| |
| - built-in type resolution |
| - registration by numeric id or by `namespace + typeName` |
| - serializer lookup |
| - struct metadata lookup |
| - type metadata encoding and decoding |
| - canonical encoded meta strings for package names, type names, and field names |
| - encoded-name lookup for named type resolution |
| - wire type decisions for struct, compatible struct, enum, ext, and union forms |
| |
| In Java xlang mode the concrete implementation is `XtypeResolver`. In Dart the |
| same ownership stays behind the internal `TypeResolver`. |
| |
| Serializers do not resolve class metadata themselves. They ask the current |
| context to read or write nested values, and the context delegates type work to |
| `TypeResolver`. |
| |
| ## Root Frame Responsibilities |
| |
| Every root payload starts with a one-byte bitmap written and read by `Fory` |
| itself, not by serializers. |
| |
| Current xlang root bits: |
| |
| | Bit | Meaning | |
| | --- | -------------------------- | |
| | `0` | null root payload | |
| | `1` | xlang payload | |
| | `2` | out-of-band buffers in use | |
| |
| Keep the root bitmap separate from per-object ref markers: |
| |
| - the root bitmap describes the whole payload |
| - ref flags describe one nested value at a time |
| |
| ## Serialization Flow |
| |
| ### Root write path |
| |
| The current root write flow is: |
| |
| 1. `Fory.serialize(...)` or `serializeTo(...)` prepares the target buffer. |
| 2. `Fory` calls `writeContext.prepare(...)`. |
| 3. `Fory` writes the root bitmap. |
| 4. `Fory` delegates the root object to `WriteContext`. |
| 5. `writeContext.reset()` runs in `finally`. |
| |
| For a non-null root value, `WriteContext.writeRootValue(...)` performs: |
| |
| 1. ref/null framing |
| 2. type metadata write |
| 3. payload write |
| |
| Payload serializers are responsible only for the payload of their type. They do |
| not write the root bitmap and they do not own registration or type-header |
| encoding. |
| |
| ### Nested writes use `WriteContext` |
| |
| Important rules: |
| |
| - nested serializers must use `WriteContext` helpers such as `writeRef(...)`, |
| `writeNonRef(...)`, and container helpers when they need ref handling or type |
| metadata |
| - repeated primitive writes should go directly through the buffer |
| - nested serializer flow should stay straight-line; do not add internal |
| `try/finally` blocks just to clean per-operation state |
| - top-level `Fory.serialize(...)` owns the operation reset `finally` |
| |
| ## Deserialization Flow |
| |
| ### Root read path |
| |
| The current root read flow mirrors the write flow: |
| |
| 1. `Fory.deserialize(...)` or `deserializeFrom(...)` reads the root bitmap. |
| 2. null roots return immediately. |
| 3. `Fory` validates xlang mode and other root framing requirements. |
| 4. `Fory` calls `readContext.prepare(...)`. |
| 5. `Fory` delegates to `ReadContext`. |
| 6. `readContext.reset()` runs in `finally`. |
| |
| ### `ReadContext` owns ref reservation and payload materialization |
| |
| `ReadContext.readRef()` performs the normal xlang read sequence: |
| |
| 1. consume the next ref marker |
| 2. return `null` or a back-reference immediately when appropriate |
| 3. reserve a fresh read ref id for new reference-tracked values |
| 4. read type metadata |
| 5. read the payload |
| 6. bind the reserved read ref id to the completed object |
| |
| Primitive and string-like hot paths should read directly from the buffer; |
| complex payloads delegate to the resolved serializer. |
| |
| ### Nested reads use `ReadContext` |
| |
| Important rules: |
| |
| - serializers that allocate the result object early must call |
| `context.reference(obj)` before reading nested children that may refer back to |
| it |
| - nested serializer flow should stay straight-line; do not add internal |
| `try/finally` blocks just to restore operation-local state |
| - top-level `Fory.deserialize(...)` owns the operation reset `finally` |
| |
| ## Depth Tracking |
| |
| `WriteContext` and `ReadContext` track logical object depth explicitly. |
| `increaseDepth()` enforces `Config.maxDepth`. |
| |
| Depth should stay explicit on the contexts rather than relying on the native |
| call stack alone. At the same time, depth cleanup should not depend on nested |
| `try/finally` blocks throughout serializer code. Top-level context reset must be |
| able to recover operation-local state after failures. |
| |
| ## Struct Compatibility |
| |
| Struct-specific schema/version framing and compatible-field staging belong in |
| the struct serializer layer, not on `Fory` and not on the public serializer |
| API. |
| |
| In Dart that internal owner is `StructCodec`. |
| |
| `StructCodec` is responsible for: |
| |
| - schema-hash framing when compatibility mode is off and version checks are on |
| - compatible-struct field remapping when compatibility mode is on |
| - caching compatible write and read layouts |
| - providing compatible write/read slot state to generated serializers |
| - remembering remote struct metadata after successful reads |
| |
| When `Config.compatible` is enabled and the struct is marked evolving: |
| |
| - the wire type uses the compatible struct form |
| - the runtime writes shared TypeDef metadata |
| - reads map incoming fields by identifier and skip unknown fields |
| |
| When `compatible` is disabled and `checkStructVersion` is enabled: |
| |
| - the runtime writes the schema hash for struct payloads |
| - the read side checks that hash before reading fields |
| |
| ## Meta Strings And Shared Type Metadata |
| |
| Two explicit pieces of state back xlang type metadata: |
| |
| - `MetaStringWriter` and `MetaStringReader` deduplicate and decode namespace |
| and type-name strings |
| - shared TypeDef write/read state tracks announced compatible struct metadata |
| |
| Ownership rules: |
| |
| - canonical encoded names live in `TypeResolver` |
| - per-operation dynamic meta-string ids live on `MetaStringWriter` and |
| `MetaStringReader` |
| - shared type-definition tables are operation-local context state |
| |
| ## Enums In Xlang Mode |
| |
| In xlang mode, enums are serialized by numeric tag, not by name. |
| |
| In Java: |
| |
| - the default tag is the declaration ordinal |
| - `@ForyEnumId` can override that with a stable explicit tag |
| - `serializeEnumByName(true)` affects native Java mode, not xlang mode |
| |
| Other runtimes should preserve the same wire rule even if the configuration or |
| annotation surface differs. |
| |
| ## Out-Of-Band Buffer Objects |
| |
| Buffer-object handling follows the same split: |
| |
| - one root bit advertises whether out-of-band buffers are in play |
| - nested buffer-object payloads still decide in-band vs out-of-band one value at |
| a time |
| - serializers use read/write context helpers rather than bypassing the runtime |
| |
| ## Code Generation |
| |
| The normal Dart integration path is: |
| |
| 1. annotate structs with `@ForyStruct` |
| 2. annotate field overrides with `@ForyField` |
| 3. run `build_runner` |
| 4. from the source library, bind the generated metadata privately and register |
| generated types through `Fory.register(...)` |
| |
| Generated code should emit: |
| |
| - private serializer classes |
| - private metadata constants |
| - private generated installation helpers per annotated library |
| - generated binding installation that keeps serializer factories private |
| |
| Generated code should not create a public global registry or a second public API |
| family. |
| |
| ## Directory Layout |
| |
| Under each Dart package `lib/` tree, only one nested source layer is allowed. |
| |
| Allowed: |
| |
| - `lib/fory.dart` |
| - `lib/src/<file>.dart` |
| - `lib/src/<area>/<file>.dart` |
| |
| Not allowed: |
| |
| - `lib/src/<area>/<subarea>/<file>.dart` |
| |
| ## Serializer Design Rules For New Runtimes |
| |
| Any new xlang runtime should follow these rules even if its surface API looks |
| different: |
| |
| 1. Keep root operations on the runtime facade and nested payload work on |
| explicit read and write contexts. |
| 2. Keep reference tracking behind dedicated read-side and write-side services |
| so the disabled path stays cheap. |
| 3. Make serializers payload-only. Type metadata, registration, and root |
| framing belong to the runtime and type resolver layers. |
| 4. Track per-operation state explicitly. Do not rely on ambient thread-local |
| runtime state. |
| 5. Reserve read reference IDs before materializing new objects, and bind |
| partially built objects as soon as a nested child may refer back to them. |
| 6. Keep operation setup and operation cleanup separate. `prepare(...)` binds |
| the current operation inputs, and `reset()` clears operation-local state. |
| 7. Preserve the separation between the root bitmap, per-object ref flags, type |
| headers, and payload bytes. |
| 8. Keep internal naming in the serialization domain. Prefer words like |
| `codec`, `binding`, `layout`, and `slots`; avoid RPC-style terms such as |
| `session` or vague control-flow terms such as `plan`. |
| 9. After any xlang protocol or ownership change, run the cross-language test |
| matrix and update both this guide and |
| [Xlang Serialization Spec](xlang_serialization_spec.md). |
| |
| ## Validation |
| |
| For Dart runtime changes, run at minimum: |
| |
| ```bash |
| cd dart |
| dart run build_runner build --delete-conflicting-outputs |
| dart analyze |
| dart test |
| ``` |
| |
| For generated consumer coverage, also run: |
| |
| ```bash |
| cd dart/packages/fory-test |
| dart run build_runner build --delete-conflicting-outputs |
| dart test |
| ``` |