blob: 371c17e026beee069ab08eb0207d01eb6ca3e326 [file] [log] [blame] [view]
# 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](https://fuchsia.googlesource.com/fuchsia/) 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](#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 | ✅ | ✅ | [⚠️](#library-declaration-remove) | ❌ | ❌ | -- | -- |
| protocol | method | ✅ | [⚠️](#protocol-method-add) | [⚠️](#protocol-method-remove) | [⚠️](#protocol-method-rename) | ❌ | ❌ | -- |
| method | parameter | ❌ | ❌ | ❌ | [⚠️](#method-parameter-rename) | ❌ | -- | -- |
| struct | field | ❌ | ❌ | ❌ | ❌ | ❌ | -- | ✅ |
| table | field | ✅ | [✅](#table-field-add) | [✅️](#table-field-remove) | [⚠️](#table-field-rename) | ❌ | ❌ | -- |
| union | variant | ✅ | [⚠️](#union-variant-add) | [⚠️](#union-variant-remove) | [⚠️](#union-variant-rename) | ❌ | ❌ | -- |
| enum | member | ✅ | [⚠️](#enum-member-add) | [⚠️](#enum-member-remove) | [⚠️](#enum-member-rename) | ❌ | -- | ✅ |
| bits | member | ✅ | [⚠️](#bits-member-add) | [⚠️](#bits-member-remove) | [⚠️](#bits-member-rename) | ❌ | -- | ✅ |
| const | value | -- | -- | -- | -- | ❌ | -- | [✅](#const-value-default-value) |
| alias | type | -- | -- | -- | [⚠️](#type-alias-rename) | [⚠️](#type-alias-change-type) | -- | -- |
| _all_ | attribute | -- | [⚠️](#attributes) | [⚠️](#attributes) | -- | -- | -- | -- |
| type | constraint | -- | [⚠️](#constraints) | [⚠️](#constraints) | -- | -- | -- | -- |
| _decl_ | modifier | -- | [⚠️](#modifiers) | [⚠️](#modifiers) | -- | -- | -- | -- |
*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]`][transitional]. Once all implementations of the new method are
in place, you can remove the [`[Transitional]`][transitional] attribute.
Examples: adding [an event][example-protocol-event-add],
[a method][example-protocol-method-add].
### 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]`][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]`][transitional] attribute. Read more about [how
`[Transitional]` impacts the Rust bindings][transitional-rust].
Examples: removing [an event][example-protocol-event-remove],
[a method][example-protocol-method-remove].
### Renaming a method {#protocol-method-rename}
**ABI**
Method renames can be made safe with use of the [`[Selector = "..."]`
attribute][selector].
**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][example-table-member-add].
### 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][example-table-member-remove].
### 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](#switch-evolvability).
Example: [adding a union variant][example-union-member-add].
### 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](#switch-evolvability).
Example: [removing a union variant][example-union-member-remove].
### 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](#switch-evolvability).
Example: [adding an enum member][example-enum-member-add].
### 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](#switch-evolvability).
Ensure that no uses of this enum member exists.
Example: [removing an enum member][example-enum-member-remove].
### 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][example-bits-member-add].
### 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][example-bits-member-remove].
### 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][soft transitions].
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`][example-bits-strict-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][bindings-ref].
* 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`][example-bits-flexible-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](#switch-evolvability).
* 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]`][unknown-attr]
attribute.
Example: [changing an enum declaration from `strict` to
`flexible`][example-enum-strict-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][bindings-ref], such
as uses of the unknown placeholder, must be removed first.
Example: [changing an enum declaration from `flexible` to
`strict`][example-enum-flexible-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][bindings-ref] for the union must be
removed before it can be changed to `strict`.
Example: changing a union declaration from [`strict` to
`flexible`][example-union-strict-flexible], or [`flexible` to
`strict`][example-union-flexible-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][rfc-0057-motivation] 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](#protocol-method-remove).
### 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`][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][lexicon-type].
**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)]`](https://doc.rust-lang.org/stable/rust-by-example/attribute/unused.html).
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][lexicon-tag]). This is often an enum in
languages that do not support ADTs like C++.
<!-- xrefs -->
[bindings-ref]: /docs/reference/fidl/bindings/overview.md
[example-bits-flexible-strict]: /docs/development/languages/fidl/guides/compatibility/bits_flexible_strict.md
[example-bits-member-add]: /docs/development/languages/fidl/guides/compatibility/bits_member_add.md
[example-bits-member-remove]: /docs/development/languages/fidl/guides/compatibility/bits_member_remove.md
[example-bits-strict-flexible]: /docs/development/languages/fidl/guides/compatibility/bits_strict_flexible.md
[example-enum-flexible-strict]: /docs/development/languages/fidl/guides/compatibility/enum_flexible_strict.md
[example-enum-member-add]: /docs/development/languages/fidl/guides/compatibility/enum_member_add.md
[example-enum-member-remove]: /docs/development/languages/fidl/guides/compatibility/enum_member_remove.md
[example-enum-strict-flexible]: /docs/development/languages/fidl/guides/compatibility/enum_strict_flexible.md
[example-protocol-event-add]: /docs/development/languages/fidl/guides/compatibility/protocol_event_add.md
[example-protocol-event-remove]: /docs/development/languages/fidl/guides/compatibility/protocol_event_remove.md
[example-protocol-method-add]: /docs/development/languages/fidl/guides/compatibility/protocol_method_add.md
[example-protocol-method-remove]: /docs/development/languages/fidl/guides/compatibility/protocol_method_remove.md
[example-table-member-add]: /docs/development/languages/fidl/guides/compatibility/table_member_add.md
[example-table-member-remove]: /docs/development/languages/fidl/guides/compatibility/table_member_remove.md
[example-union-flexible-strict]: /docs/development/languages/fidl/guides/compatibility/union_flexible_strict.md
[example-union-member-add]: /docs/development/languages/fidl/guides/compatibility/union_member_add.md
[example-union-member-remove]: /docs/development/languages/fidl/guides/compatibility/union_member_remove.md
[example-union-strict-flexible]: /docs/development/languages/fidl/guides/compatibility/union_strict_flexible.md
[rfc-0057-motivation]: /docs/contribute/governance/rfcs/0057_default_no_handles.md#motivation
[lexicon-tag]: /docs/reference/fidl/language/lexicon.md#union-terms
[lexicon-type]: /docs/reference/fidl/language/lexicon.md#type-terms
[Platform Versioning]: /docs/contribute/governance/rfcs/0002_platform_versioning.md
[rust-bindings-tables]: /docs/reference/fidl/bindings/rust-bindings.md#types-tables
[rust-enum-macro]: /docs/reference/fidl/bindings/rust-bindings.md#types-enums
[selector]: /docs/reference/fidl/language/attributes.md#selector
[soft transitions]: /docs/contribute/governance/rfcs/0002_platform_versioning.md#terminology
[transitional-rust]: /docs/reference/fidl/bindings/rust-bindings.md#transitional
[transitional]: /docs/reference/fidl/language/attributes.md#transitional
[unknown-attr]: /docs/reference/fidl/language/attributes.md#unknown