Many changes to the Fuchsia platform involve changing FIDL APIs which 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.
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.
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 which 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 | -- | -- | -- | -- | ❌ | -- | ✅ |
all | attribute | -- | ⚠️ | ⚠️ | -- | -- | -- | -- |
type | constraint | -- | ⚠️ | ⚠️ | -- | -- | -- | -- |
decl | modifier | -- | ⚠️ | ⚠️ | -- | -- | -- | -- |
Legend:
- ✅ = Safe
- ⚠️ = Careful (follow linked advice)
- ❌ = Unsafe
- -- = Not Applicable
Library
Removing a library declaration
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
Adding a method to a protocol
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.
Removing a method from a protocol
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.
Renaming a method
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
Renaming a method parameter
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
Adding a table field
ABI: It is binary-compatible to add a table field.
API: It is source-compatible to add a table field.
Removing a table field
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.
Renaming a table field
ABI: It is binary-compatible to rename a table field.
API: It is not source-compatible to rename a table field.
Union
Adding a union variant
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: Care must be taken to transition switches on the union tag.
Removing a union variant
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: Care must be taken to transition switches on the union tag.
Renaming a union variant
ABI: It is binary-compatible to rename a union variant.
API: It is not source-compatible to rename a union variant.
Enum
Adding an enum member
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.
Removing an enum member
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.
Renaming an enum member
ABI: It is binary-compatible to rename an enum member.
API: It is not source-compatible to rename an enum member.
Bits
Adding a bits member
ABI: It is binary-compatible to add a bits member. 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.
Removing a bits member
ABI: It is binary-compatible to remove a bits member. 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.
Renaming a bits member
ABI: It is binary-compatible to rename a bits member.
API: It is not source-compatible to rename a bits member.
Constant
Updating value of constants
It is safe 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 which would fail with the updated value.
Modifiers
Strict vs 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
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 aclass
(which cannot be used as a non-type template parameter).
- Any usages of the bits type as a template parameter must be removed first,
since strict bits are generated as an
Changing a bits declaration from flexible
to strict
is:
- Source-compatible Go, and Dart
- Source-incompatible in Rust, HLCPP and LLCPP.
- Transitions from
flexible
tostrict
will require removing usages offlexible
-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)]
.
- Transitions from
Enums
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 amatch
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 aclass
, which cannot be used as a non-type template parameter. - In HLCPP, the bit mask is a
const
in the top level library namespace for strict bits, but astatic const
member of the generated class for flexible bits.
- In Rust, any
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.
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.
- To make this change, any usages of
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
.
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 FTP-057 for context).
General advice
Safely removing members
Most soft transitions follow this basic shape:
- Ensure that the element is not used or referenced
- Remove the element
In a successful soft transition, only the second step is dangerous.
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
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]
[MaxBytes]
[MaxHandles]
- `[Unknown]
Constraints
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 (e.g. changing a vector's maximum allowable size to
grow from vector<T>:128
to vector<T>:256
), 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 which would then be rejected at runtime.
API: Relaxing or tightening constraints is source-compatible.
Evolving switch on enums, or union tag
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.