Skip to content

Latest commit

 

History

History
593 lines (398 loc) · 22.2 KB

File metadata and controls

593 lines (398 loc) · 22.2 KB

FIDL binary-compatibility (ABI) and source-compatibility (API) guide

Many changes to the Fuchsia platform involve changing FIDL APIs that have already been published. Unless managed carefully, these changes risk breaking existing usages. Failed changes manifest in the following ways:

  • Source incompatibility: Users can no longer build against the generated code.
  • Binary incompatibility: Consuming programs can no longer understand each other at runtime.

The Fuchsia project requires that changes to published FIDL libraries are both source-compatible and binary-compatible for partners.

Note: Some changes are binary-compatible, yet require a specific transition path to avoid runtime validation issues. Binary-compatibility indicates that two peers have the same understanding of how to read or write the data, though these two peers may disagree on which values are deemed valid. As an example, a uint32 is binary-compatible with a enum : uint32, even though the enum has runtime validation that restricts the domain to only the specific values identified by the enum members.

Which changes to a FIDL API are safe?

For the purpose of describing interface compatibility, FIDL libraries are made up of declarations. Each declaration has a name, type, attributes, and members. Once an API is used outside of fuchsia.git the safest assumption is that all changes to it must be both binary-compatible and source-compatible with current clients. This usually means evolving libraries using soft transitions, where the backwards-incompatible portions of a change are left to the end when they will have no impact because all clients have already been migrated. See safely removing members for more information on the most common soft transition pattern.

Note: Source-compatibility guarantees are only guaranteed under "normal" circumstances. It is possible to write code that causes these guarantees to be violated, e.g. static asserts.

Safe changes to members

Aside from a declaration's name and attributes, all changes to its contract are expressed in terms of changes to the declaration's members. This relationship also means incompatible changes to a declaration become incompatible changes to all the FIDL libraries that depend on that declaration, not just direct consumers of the original library's generated bindings.

All operations are safe to perform if you are certain that all consumers can be migrated atomically, i.e. they are all in the same source repository as the library definition. Otherwise, these operations must be completed as the final stage in a soft transition after all clients have been migrated away.

The table below summarizes various member changes and their respective safety level when some clients cannot be migrated atomically:

Parent Change Target Reorder Lines Add Remove Rename Change Type Change Ordinal (Default) Value
library declaration ⚠️ -- --
protocol method ⚠️ ⚠️ ⚠️ --
method parameter ⚠️ -- --
struct field --
table field ✅️ ⚠️ --
union variant ⚠️ ⚠️ ⚠️ --
enum member ⚠️ ⚠️ ⚠️ --
bits member ⚠️ ⚠️ ⚠️ --
const value -- -- -- -- --
alias type -- -- -- ⚠️ ⚠️ -- --
all attribute -- ⚠️ ⚠️ -- -- -- --
type constraint -- ⚠️ ⚠️ -- -- -- --
decl modifier -- ⚠️ ⚠️ -- -- -- --

Legend:

  • ✅ = Safe
  • ⚠️ = Careful (follow linked advice)
  • ❌ = Unsafe
  • -- = Not Applicable

Library {#library}

Removing a library declaration {#library-declaration-remove}

ABI

It is binary-compatible to remove a library declaration.

API

Before removing a library declaration, ensure that no uses of this declaration exists.

Protocols {#protocol}

Adding a method to a protocol {#protocol-method-add}

ABI

It is binary-compatible to add a method to a protocol.

API

To safely add a method to a protocol, mark the new method with [Transitional]. Once all implementations of the new method are in place, you can remove the [Transitional] attribute.

Examples: adding an event, a method.

Removing a method from a protocol {#protocol-method-remove}

ABI

It is binary-compatible to remove a method from a protocol.

API

To safely remove a method from a protocol, start by marking the method with [Transitional]. Once this has fully propagated, you can remove all implementations of the method, then remove the method from the FIDL protocol.

Note: When using the Rust bindings, you need to manually add catch-all cases (_) to all the match statements rather than rely on the [Transitional] attribute. Read more about how [Transitional] impacts the Rust bindings.

Examples: removing an event, a method.

Renaming a method {#protocol-method-rename}

ABI

Method renames can be made safe with use of the [Selector = "..."] attribute.

API

It is not possible to rename a method in a source-compatible way.

Method {#method}

Renaming a method parameter {#method-parameter-rename}

ABI

It is binary-compatible to rename a method parameter.

API

Bindings typically rely on positional arguments, such that renaming a method parameter is source-compatible.

Table {#table}

Adding a table field {#table-field-add}

ABI

It is binary-compatible to add a table field.

API

It is source-compatible to add a table field.

Example: adding a table member.

Removing a table field {#table-field-remove}

ABI

It is binary-compatible to remove a table field.

API

There must not be any use of the field to ensure a source-compatible removal.

Example: removing a table member.

Renaming a table field {#table-field-rename}

ABI

It is binary-compatible to rename a table field.

API

It is not source-compatible to rename a table field.

Union {#union}

Adding a union variant {#union-variant-add}

ABI

It is binary-compatible to add a union variant. To ensure the added union variant is not rejected during runtime validation, it must have propagated to readers ahead of it being used by writers.

API

For strict unions, care must be taken to transition switches on the union tag.

Example: adding a union variant.

Removing a union variant {#union-variant-remove}

ABI

It is binary-compatible to remove a union variant. To ensure the removed union variant is not rejected during runtime validation, no writer may use the union variant when it is removed.

API

For strict unions, care must be taken to transition switches on the union tag.

Example: removing a union variant.

Renaming a union variant {#union-variant-rename}

ABI

It is binary-compatible to rename a union variant.

API

It is not source-compatible to rename a union variant.

Enum {#enum}

Adding an enum member {#enum-member-add}

ABI

It is binary-compatible to add an enum member. To ensure the added enum member is not rejected during runtime validation, it must have propagated to readers ahead of it being used by writers.

API

Care must be taken to transition switches on the enum.

Example: adding an enum member.

Removing an enum member {#enum-member-remove}

ABI

It is binary-compatible to remove an enum member. To ensure the removed enum member is not rejected during runtime validation, no writer may use the enum member when it is removed.

API

Care must be taken to transition switches on the enum. Ensure that no uses of this enum member exists.

Example: removing an enum member.

Renaming an enum member {#enum-member-rename}

ABI

It is binary-compatible to rename an enum member.

API

It is not source-compatible to rename an enum member.

Bits {#bits}

Adding a bits member {#bits-member-add}

ABI

It is binary-compatible to add a bits member. For strict bits, to ensure the added bits member is not rejected during runtime validation, it must have propagated to readers ahead of it being used by writers.

API

It is source-compatible to add a bits member.

Example: adding a bits member.

Removing a bits member {#bits-member-remove}

ABI

It is binary-compatible to remove a bits member. For strict bits, to ensure the removed bits member is not rejected during runtime validation, no writer may use the bits member when it is removed.

API

It is source-compatible to remove a bits member. Ensure that no uses of this bits member exists.

Example: removing a bits member.

Renaming a bits member {#bits-member-rename}

ABI

It is binary-compatible to rename a bits member.

API

It is not source-compatible to rename a bits member.

Constant {#const}

Updating value of constants {#const-value-default-value}

ABI

It is sometimes binary-compatible to update the value of a const declaration. If a constant value affects the public interface semantics (for example, by representing a runtime invariant in the interface), changing the constant value is binary-incompatible due to mismatched expectations between peers on different versions.

API

It is usually source-compatible to update the value of a const declaration. In rare circumstances, such a change could cause source-compatibility issues if the constant is used in static asserts that would fail with the updated value.

Type alias {#type-alias}

Renaming a type alias {#type-alias-rename}

ABI

It is ABI compatible to rename a type alias.

API

It is not source-compatible to rename a type alias.

Changing the underlying type of a type alias {#type-alias-change-type}

ABI

Typically, it is not ABI compatible to change the underlying type of a type alias. However, if the original underlying type and the replacement underlying type are ABI compatible, then a change is ABI compatible.

API

It is not source-compatible to change the underlying type of a type alias.

Modifiers {#modifiers}

Strict vs flexible {#strict-flexible}

Changing the strictness modifier of an enum, bits, or union declaration is binary-compatible. Changing from flexible to strict may cause runtime validation errors as unknown data for a previously flexible type will start being rejected.

Generally, changing the strictness on a declaration is source-incompatible, but possible to soft transition. Details for each declaration and binding are provided below.

Bits

strict to flexible

Changing a bits declaration from strict to flexible is:

  • Source-compatible in LLCPP, Rust, Go, and Dart.
  • Source-incompatible in HLCPP.
    • Any usages of the bits type as a template parameter must be removed first, since strict bits are generated as an enum class and flexible bits are generated as a class (which cannot be used as a non-type template parameter).

Example: changing a bits declaration from strict to flexible.

flexible to strict

Changing a bits declaration from flexible to strict is:

  • Source-compatible Go, and Dart
  • Source-incompatible in Rust, HLCPP and LLCPP.
    • Transitions from flexible to strict will require removing usages of flexible-only APIs.
    • In Rust, certain methods are provided for both strict and flexible bits, but usages for strict bits cause a deprecation warning during compation, which could become errors if using -Dwarning or #![deny(warnings)].
    • In HLCPP, the bit mask is a const in the top level library namespace for strict bits, but a static const member of the generated class for flexible bits.

Example: changing a bits declaration from flexible to strict.

Enums

strict to flexible

Changing an enum declaration from strict to flexible is:

  • Source-compatible in Go and Dart.
  • Source-incompatible in Rust, HLCPP, and LLCPP.
    • In Rust, any match statements must be updated to handle unknown enum values when using a match statement.
    • In HLCPP and LLCPP, any uses of the enum as a template parameter must be removed first. This is because strict enums are generated as an enum class whereas flexible enums are generated as a class, which cannot be used as a non-type template parameter.

After changing from strict to flexible, care must be taken to correctly handle any unknown enums.

strict enums that already have a specific member to represent the unknown case can transition to being flexible by using the [Unknown] attribute.

Example: changing an enum declaration from strict to flexible.

flexible to strict

Changing an enum declaration from flexible to strict is:

  • Source-incompatible in all bindings.
  • To make this change, any usages of flexible-only APIs, such as uses of the unknown placeholder, must be removed first.

Example: changing an enum declaration from flexible to strict.

Unions

Changing a union declaration from strict to flexible is source-compatible, and changing from flexible to strict is source-incompatible. To perform the latter, any usages flexible-only APIs for the union must be removed before it can be changed to strict.

Example: changing a union declaration from strict to flexible, or flexible to strict.

Value vs resource {#value-vs-resource}

Adding or removing the resource modifier on a struct, table, or union is binary-compatible. Removing the resource modifier may cause runtime validation errors: flexible types, such as tables and flexible unions, will now fail to decode any unknown data (i.e. unknown variants for flexible unions and unknown fields for tables) that contains handles. Note that this particular scenario does not apply to LLCPP because LLCPP never stores unknown handles.

Adding or removing the resource modifier is not source-compatible. Furthermore, bindings are encouraged to diverge APIs if they can leverage the value type versus resource type distinction for specific benefits in the target language (see RFC-0057 for context).

General advice

Safely removing members {#safely-removing-members}

Most soft transitions follow this basic shape:

  1. Ensure that the element is not used or referenced
  2. Remove the element

In a successful soft transition, only the second step is dangerous.

Note: Safely removing methods is more involved, see removing a method from a protocol.

Renames {#renames}

Renaming declarations themselves is a source-incompatible change. Similarly, renaming declaration members (e.g. a struct field) is source-incompatible.

Often, a source-compatible rename is possible following the long process of adding a duplicate member with the desired name, switching all code to shift from the old member to the new member, then deleting the old member. This approach can be quite direct with table fields for instance.

Renames are binary-compatible, except in the case of libraries, protocols, methods and events. See the @selector attribute for binary-compatible renames of these.

Attributes {#attributes}

Removing @discoverable is a source-incompatible change. You first need to ensure that there are no references to the generated protocol name before removing this attribute.

Adding or changing @selector is a binary-incompatible change on its own, but can be used in the same change as method renames to preserve binary-compatibility.

Removing @transitional is a source-incompatible change. You first need to ensure that all implementations of the method are in place.

Adding or changing @transport is a source-incompatible and binary-incompatible change.

Changes to the following attributes have no effect on compatibility, although they often accompany other incompatible changes:

  • @deprecated (although it may in the future if/when implemented)
  • @doc
  • @max_bytes
  • @max_handles
  • @unknown

Constraints {#constraints}

For more information on what a constraint is in FIDL, see Type, layout, constraint.

ABI

Relaxing or tightening constraints is binary-compatible. However, when evolving constraints, care must be taken to transition readers or writers to avoid runtime validation issues.

When relaxing a constraint, all readers must transition ahead of writers to avoid values being rejected at runtime. Conversely, when tightening a constraint, all writers must transition ahead of readers to avoid emitting values that would then be rejected at runtime.

For instance:

  • Growing a vector's maximum allowable size from vector<T>:128 to vector<T>:256 relaxes a constraints, i.e. move values will be allowed. As a result, readers must be transitioned ahead of writers.
  • Restricting an optional handle handle? to be required handle tightens a constraint, optional handles that were accepted before will no longer be. As a result, writers must be transitioned ahead of readers.

API

Relaxing or tightening constraints is source-compatible.

Evolving switch on enums, or union tag {#switch-evolvability}

When adding an enum member (or adding a union variant), any switch on the enum (respectively the union tag) must first evolve to handle the soon to be added member (resp. variant). This is done by adding a default case for instance, or a catch-all _ match. Depending on compiler flags, this may require additional attributes such as #[allow(dead_code)].

Similarly, when removing an enum member (or removing a union variant), any switch on the enum (respectively the union tag) must first evolve to replace the soon to be removed member (resp. variant) by a default case.

Note: A union tag is the discriminator indicating which variant is currently held by the union (see lexicon). This is often an enum in languages that do not support ADTs like C++.