# Adapters ## Overview Thrift Adapter is the standard customization method to allow users to use their own custom types on Thrift generated structs and fields. Thrift Adapter **must** be able to convert from [standard type](../glossary/#kinds-of-types) to [adapted type](../glossary/#kinds-of-types) and vice versa. Thrift Adapter is designed so that it can be enabled per each langauge without intefering wire format. Thrift Adapter serves as a building blocks for other Thrift features, such as Protocol Object. Thrift Adapter **may** provide customization for [value operators](operators.md), including comparison, equality, hash, clear, serialized size, serialize, and deserialize. This allows Thrift Adapter to avoid conversion back to standard type for evaluation and manipulation. Thrift Adapters allow the native type used in Thrift code gen, to be customized without writing a lot of code or digging into Thrift internals. This decouples changes in Thrift from the functionality of these custom objects, so they can be maintained directly by individual teams, writing their own libraries to hook into Thrift to achieve their desired API. To use Adapter, we can apply structured annotation `@{lang}.Adapter{name = "..."}` to a field or a typedef, with the name of adapter class. Adapter provides `from_thrift` and `to_thrift` APIs. Here is an example in C++ (The APIs are similar in other languages) ```cpp struct Adapter { static AdaptedType fromThrift(DefaultType); static DefaultType toThrift(const AdaptedType&); }; ``` This changes the type used by Thrift from the [default type](../glossary/#kinds-of-types) to [adapted type](../glossary/#kinds-of-types). ## Definitions ### Type Adapter Type Adapter **must** accept standard type and convert to adapted type. It **must** convert from standard type to adapted type and from adapted type to standard type. Type Adapter **must** convert from adapted type to another adapted type when it is used for composition. #### Type Wrapper Type Wrapper is a special case of Type Adapter, where adapted type is restricted to be a wrapper around standard type. Type Wrapper **must** convert from adapted type to standard type. Type Wrapper **may** convert from standard type to adapted type. Type Wrapper **must** convert from adapted type to another adapted type when it is used for composition. Each language **may** distinguish Type Adapter and Type Wrapper. ### Field Adapter Field Adapter **must** accept standard type and field context which consists of parent struct and field id. Field Adapter **must** convert from standard to adapted type and from adapted type to standard type. Field Adapter **must** convert from adapted type to another adapted type when it is used for composition. #### Field Wrapper Field Wrapper is a special case of Field Adapter, where adapted type is restricted to be a wrapper around standard type. Field Wrapper **must** convert from adapted type to standard type. Field Wrapper **may** convert from standard type to adapted type. Field Wrapper **must** convert from adapted type to another adapted type when it is used for composition. Each language **may** distinguish Field Adapter and Field Wrapper. ## C++ C++ adapter can be enabled via `cpp.Adapter` annotation. Users **must** provide a fully qualified C++ typename for `name`, e.g. ```cpp // In C++ Header namespace apache::thrift::test { struct BitsetAdapter { static std::bitset<32> fromThrift(std::uint32_t t) { return {t}; } static std::uint32_t toThrift(std::bitset<32> t) { return t.to_ullong(); } }; } ``` ```thrift // In Thrift file include "thrift/annotation/cpp.thrift" cpp_include "BitsetAdapter.h" struct Foo { @cpp.Adapter{name = "::apache::thrift::test::BitsetAdapter"} 1: i32 flags; } ``` ```cpp // In C++ file Foo foo; foo.flags()->set(0); // set 0th bit ``` :::note cpp.Adapter will break Thrift py3 usage. Please migrate to new Thrift Python to use cpp.Adapter and python.Adapter. ::: :::note Adapted types can not be used directly with Thrift APIs. Values must be converted back to Thrift types. (e.g. `apache::thrift::CompactSerializer::serialize(Adapter::toThrift(adaptedValue))`) ::: ### Type Adapter Type adapter’s APIs are mentioned above. It’s worth noting that API doesn’t need to be identical — it can be template function. In addition, the user can use reference to avoid making extra copy/move. e.g. ```cpp struct ThriftTypeAdapter { static AdaptedType fromThrift(DefaultType&& thrift); static const DefaultType& toThrift(const AdaptedType& native); }; ``` ### Field Adapter Field adapter requires the user to provide the following APIs. ```cpp struct ThriftFieldAdapter { template static void construct(AdaptedType& thrift, Context ctx); template static AdaptedType fromThriftField(DefaultType thrift, Context ctx); static {const DefaultType& | DefaultType} toThrift( const AdaptedType& native); }; ``` `Context` is an instantiation of [FieldAdapterContext](https://www.internalfb.com/code/fbsource/[388310ab2f4d793c6b1056369cedfa9fd12ff51b]/fbcode/thrift/lib/cpp2/Adapt.h?lines=31) which can access field id and parent struct. e.g. ```cpp template AdaptedType ThriftFieldAdapter::fromThriftField(DefaultType thrift, Context ctx) { constexpr int16_t kFieldId = Context::kFieldId; auto& object s = ctx.object; // ... } ``` Both type adapter and field adapter use `cpp.Adapter` API to enable. We will try to invoke field adapter API first. If it fails, we will fall back to type adapter. ### Compose Type Adapter can be applied to a typedef, struct, or field. Field Adpater can be only applied to a field. For each typedef, field, or struct, you can only apply a single adapter. You cannot compose multiple type adapters on nested typedefs. You can only compose adapters on a field, where the type of the field is to be a typedef or struct with type adapter and either type adapter or field adapter is directly applied to the field. You cannot compose multiple field adapters. ```thrift // This would result in an error if uncommented. // @cpp.Adapter{name="::CustomTypeAdapter"} // typedef CustomInt DoubleCustomInt @cpp.Adapter{name = "::CustomTypeAdapter"} typedef i32 CustomInt struct Foo { @cpp.Adapter{name = "::CustomFieldAdapter"} 1: CustomInt field; @cpp.Adapter{name = "::CustomTypeAdapter"} 2: CustomInt field2; // This would result in an error if uncommented. // @cpp.Adapter{name = "::CustomTypeAdapter"} // @cpp.Adapter{name = "::CustomFieldAdapter"} // 3: i32 field3; } ``` More examples can be found [here](https://www.internalfb.com/code/fbsource/fbcode/thrift/test/adapter.thrift). ### Optimizations #### In-place Deserialization Deserialization for adapted types can be costly, as the deserializer creates temporaries of [standard types](../glossary/#kinds-of-types), deserializes binary into them, and uses `fromThrift` to convert them to adapted values. Thrift Adapter optimizes deserialization when `toThrift` returns mutable reference. This avoids construction of temporary values, and the deserializer directly deserialize binary to the mutable reference returned by `toThrift`. It is recommended to return a mutable reference from `toThrift` if the adapted type has access to an object of the standard type internally. The following adapter utilizes in-place deserialization: ```cpp template struct Wrapper { T value }; struct Adapter { template static Wrapper fromThrift(T&& value) { return Wrapper{std::forward(value)}; } template static decltype(auto) toThrift(W&& wrapper) { return std::forward(wrapper).value; } } ``` #### Customizations :::warning Serialization and deserialization customization should be used with caution and only produce the same wire type as the unadapted thrift type. Failing to do so can result in breaking backward compatibility or losing polyglot support. Please use `binary` field if you plan to customize serialization to non-conforming arbitrary binary. ::: Thrift Adapter allows further customization points to avoid calling `fromThrift` and `toThrift` for optimization. Assume `obj` has type `AdaptedType`. * Comparison and Equality * Equality Priority * `Adapter::equal(const AdaptedType& lhs, const AdaptedType& rhs)` * `AdaptedType::operator==(const AdaptedType& rhs)` * `DefaultType::operator==(const DefaultType& rhs)` using `Adapter::toThrift(lhs) == Adapter::toThrift(rhs)` * Comparison Priority is same as above. * Hash * Hash Priority * `std::hash(obj)` * `std::hash(Adapter::toThrift(obj))` * Clear * Clear Priority * `Adapter::clear(AdaptedType&)` * `obj = AdaptedType()` * Serialized Size * SerializeSize Priority * `Adapter::serializedSize(Protocol&, AdaptedType&)` * `Protocol::serializedSize(Adapter::toThrift(obj))` * Serialize and Deserialize * Serialization Priority * `Adapter::encode(Protocol&, const AdaptedType&)` * `op::encode(Protocol&, Adapter::toThrift(obj))` * Deserialization Priority: [In-place deserialization](#in-place-deserialization) (second option) will be phased-out soon. We recommend that you implement the `Adapter::decode` customization instead. * `Adapter::decode(Protocol&, AdaptedType&)` * `op::decode(Protocol&, Adapter::toThrift(obj))` * ``` DefaultType thrift_value; op::decode(protocol, thrift_value); obj = Adapter::fromThrift(thrift_value); ``` * Empty * Empty Priority * `Adapter::isEmpty(const AdaptedType&)` * `op::isEmpty(Adapter::toThrift(obj))` ### Other Codegen Customizations * `adaptedType`: normally the runtime can determine the result of `Adapter::fromThrift`, but sometimes doing so would result in a circular dependency or declaration order error. Setting this annotation to the result type breaks the dependency. * `underlyingName` and `extraNamespace`: when directly adapting a Thrift type, the name of Thrft type may need to be mangled to avoid colliding with the name of adapted type. If neither is specified, Thrift will use the `detail` namespace and the same name. * `moveOnly`: indicates that structs with this adapted type as a field should not have copy constructors. ## Hack ### Type Adapter #### On Typedef Hack type adapter can be enabled via [`hack.Adapter`](https://www.internalfb.com/code/fbsource/[8155292d2d170907326ad48d3a070ebe3ccb1be3]/fbcode/thrift/annotation/hack.thrift?lines=55). It should implement [`IThriftAdapter` ](https://www.internalfb.com/code/www/[1cce1dcd39b3d25482944ebc796fafb4c3a3d4fd]/flib/thrift/core/IThriftAdapter.php?lines=19-29)interface. For example ```hack // In Hack file final class TimestampToTimeAdapter implements IThriftAdapter { const type TThriftType = int; const type THackType = Time; public static function fromThrift(int $seconds)[]: Time { return Time::fromEpochSeconds($seconds); } public static function toThrift(Time $time): int { return $hack_value->asFullSecondsSinceEpoch(); } } ``` ```thrift // In Thrift file include "thrift/annotation/hack.thrift" @hack.Adapter {name = '\\TimestampToTimeAdapter'} typedef i32 i32_withAdapter; struct Document { 1: i32_withAdapter created_time; } ``` ``` // Thrift compiler will generate this for you: class Document implements \IThriftSyncStruct { ... public TimestampToTimeAdapter::THackType $created_time; ... } ``` ``` // In hack file function timeSinceCreated(Document $doc): Duration { // $doc->created_time is of type Time return Duration::between(Time::now(), $doc->created_time); } ``` #### On Field Similar to typedef, adapter can be added to fields directly. It should implement [`IThriftAdapter` ](https://www.internalfb.com/code/www/[1cce1dcd39b3d25482944ebc796fafb4c3a3d4fd]/flib/thrift/core/IThriftAdapter.php?lines=19-29)interface. Below IDL will generate the same code as above. ```thrift // In Thrift file struct Document { @hack.Adapter{name = '\\TimestampToTimeAdapter'} 1: i32 created_time; } // Thrift compiler will generate this for you: class Document implements \IThriftSyncStruct { ... public TimestampToTimeAdapter::THackType $created_time; ... } ``` ### Wrapper This annotation will let you change a type to a wrapper class. It should extend `IThriftTypeWrapper` for primitive or container types, `IThriftFieldWrapper` for fields, or `IThriftStructWrapper` for structured types. A wrapper wraps your type and allows you to execute custom code before reading/writing. When applied to a field, the field becomes private, and you can get the wrapper object via the getter method in the struct. The wrapper APIs are async so using wrapped fields will change your struct to an async struct. All the factory methods like `fromShape`, `toShape` will become async to access field value via the wrapper. See example below. ``` // An example of Field Wrapper in Hack file final class FooOnlyAllowOwnerToAccess extends IThriftFieldWrapper { <<__Override>> public static async function genToThrift( this $value, )[zoned]: Awaitable { return await $value->genUnwrap(); } <<__Override>> public static async function genFromThrift< <<__Explicit>> TThriftType__, <<__Explicit>> TThriftStruct__ as IThriftAsyncStruct, >( TThriftType__ $value, int $field_id, TThriftStruct__ $parent, )[zoned]: Awaitable> { return new MyFieldWrapper($value, $field_id, $parent); } <<__Override>> public async function genUnwrap()[zoned]: Awaitable { await genHasAccess(); return $this->value; } <<__Override>> public async function genWrap(TThriftType $value)[zoned]: Awaitable { await genHasAccess(); $this->value = $value; } public async function genHasAccess(): Awaitable { ... if ($has_access) { return; } throw new InvalidAccessException(); } public async function genAsTime()[zoned]: Awaitable