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
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. 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:
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.
Use these sources in this order:
docs/specification/xlang_serialization_spec.mdintegration_tests/For Dart, the runtime shape is centered on:
ForyWriteContextReadContextRefWriterRefReaderTypeResolverStructCodecFory is the root-operation facadeFory owns the reusable runtime services for one runtime instance.
In Dart, Fory owns exactly four runtime members:
BufferWriteContextReadContextTypeResolverIn 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:
WriteContextReadContextTypeResolverfinallyNested serializers must not call back into root serialize(...) or deserialize(...) entry points.
WriteContext and ReadContext hold operation-local stateWriteContext 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:
RefWriter or RefReaderGenerated 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.
WriteContextWriteContext owns all write-side per-operation state:
BufferRefWriterMetaStringWritertrackRef modeIt exposes one-shot primitive helpers such as:
writeBoolwriteInt32writeVarUint32These helpers are convenience methods. Serializers that perform repeated primitive IO should cache final buffer = context.buffer; and call buffer methods directly.
ReadContextReadContext owns all read-side per-operation state:
BufferRefReaderMetaStringReaderIt exposes matching one-shot primitive helpers such as:
readBoolreadInt32readVarUint32Generated struct serializers call context.reference(value) immediately after constructing the target instance so back-references can resolve to that object.
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:
trackRef is only for top-level graphs and container roots with no field metadatacontext.reference(...)TypeResolver owns:
namespace + typeNameIn 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.
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 current root write flow is:
Fory.serialize(...) or serializeTo(...) prepares the target buffer.Fory calls writeContext.prepare(...).Fory writes the root bitmap.Fory delegates the root object to WriteContext.writeContext.reset() runs in finally.For a non-null root value, WriteContext.writeRootValue(...) performs:
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.
WriteContextImportant rules:
WriteContext helpers such as writeRef(...), writeNonRef(...), and container helpers when they need ref handling or type metadatatry/finally blocks just to clean per-operation stateFory.serialize(...) owns the operation reset finallyThe current root read flow mirrors the write flow:
Fory.deserialize(...) or deserializeFrom(...) reads the root bitmap.Fory validates xlang mode and other root framing requirements.Fory calls readContext.prepare(...).Fory delegates to ReadContext.readContext.reset() runs in finally.ReadContext owns ref reservation and payload materializationReadContext.readRef() performs the normal xlang read sequence:
null or a back-reference immediately when appropriatePrimitive and string-like hot paths should read directly from the buffer; complex payloads delegate to the resolved serializer.
ReadContextImportant rules:
context.reference(obj) before reading nested children that may refer back to ittry/finally blocks just to restore operation-local stateFory.deserialize(...) owns the operation reset finallyWriteContext 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-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:
When Config.compatible is enabled and the struct is marked evolving:
When compatible is disabled and checkStructVersion is enabled:
Two explicit pieces of state back xlang type metadata:
MetaStringWriter and MetaStringReader deduplicate and decode namespace and type-name stringsOwnership rules:
TypeResolverMetaStringWriter and MetaStringReaderIn xlang mode, enums are serialized by numeric tag, not by name.
In Java:
@ForyEnumId can override that with a stable explicit tagserializeEnumByName(true) affects native Java mode, not xlang modeOther runtimes should preserve the same wire rule even if the configuration or annotation surface differs.
Buffer-object handling follows the same split:
The normal Dart integration path is:
@ForyStruct@ForyFieldbuild_runnerFory.register(...)Generated code should emit:
Generated code should not create a public global registry or a second public API family.
Under each Dart package lib/ tree, only one nested source layer is allowed.
Allowed:
lib/fory.dartlib/src/<file>.dartlib/src/<area>/<file>.dartNot allowed:
lib/src/<area>/<subarea>/<file>.dartAny new xlang runtime should follow these rules even if its surface API looks different:
prepare(...) binds the current operation inputs, and reset() clears operation-local state.codec, binding, layout, and slots; avoid RPC-style terms such as session or vague control-flow terms such as plan.For Dart runtime changes, run at minimum:
cd dart dart run build_runner build --delete-conflicting-outputs dart analyze dart test
For generated consumer coverage, also run:
cd dart/packages/fory-test dart run build_runner build --delete-conflicting-outputs dart test