This document describes FIDL's API versioning features. For guidance on how to evolve Fuchsia Platform APIs, see Fuchsia API evolution guidelines.
Summary
FIDL versioning lets you represent changes to a FIDL library over time. When
making a change, you use the @available
attribute to describe when (i.e. at
which version) the change occurs. To generate bindings, you pass the
--available
flag to fidlc specifying one or more versions.
FIDL versioning provides API versioning, not ABI versioning. There is no way to query versions at runtime. Changes may be API breaking, but they are expected to be ABI compatible. The FIDL compiler performs some basic validation, but it does not guarantee ABI compatibility.
Concepts
The unit of versioning is a group of libraries, called a platform. By
convention, libraries are named starting with the platform name. For example,
the libraries fuchsia.mem
and fuchsia.web
belong to the fuchsia
platform.
Each platform has a linear version history. A version is an integer from 1
to 2^31-1 (inclusive), or one of the special versions NEXT
and HEAD
. The
NEXT
version is used for changes that are planned to go in the next numbered
version. The HEAD
version is used for the latest unstable changes.
If a FIDL library doesn't have any @available
attributes, it belongs to the
unversioned
platform. This platform only has one version, HEAD
.
FIDL libraries in fuchsia.git
must specify an @available
attribute.
Target versions
When you target a single version, the bindings include all elements that are
available in that version as specified by @available
arguments in the FIDL
files.
When you target a set of versions, the bindings include all elements that are
available in any of the versions in the set. For elements that are
replaced
, the bindings only include the latest definition.
No matter what set of versions you target, if FIDL compilation succeeds, then it is also guaranteed to succeed for all subsets of that set, and for all possible singleton sets.
Syntax
The @available
attribute is allowed on any FIDL element. It takes
the following arguments:
Argument | Type | Description |
---|---|---|
platform |
string |
Library group name (see Concepts); only allowed on library |
added |
uint64 |
Integer, NEXT , or HEAD |
deprecated |
uint64 |
Integer, NEXT , or HEAD |
removed |
uint64 |
Integer, NEXT , or HEAD |
replaced |
uint64 |
Integer, NEXT , or HEAD |
note |
string |
Provides context for deprecated , removed , and/or replaced |
renamed |
string |
New name for removed or replaced element; only allowed on members |
There are some restrictions on the arguments:
- All arguments are optional, but at least one must be provided.
- Arguments must be literals, not references to
const
declarations. - The
removed
andreplaced
arguments are mutually exclusive. - Arguments must respect
added <= deprecated < removed
, oradded <= deprecated < replaced
. - The
added
,deprecated
,removed
, andreplaced
arguments inherit from the parent element if unspecified.
For example:
@available(added=1, deprecated=2, removed=3)
const ANSWER uint64 = 42;
If @available
is used anywhere in a library, it must also appear on the
library declaration. For single-file libraries, this is straightforward. For
libraries with two or more .fidl
files, only one file can have its library
declaration annotated. (The library is logically considered a single element
with attributes merged from each file, so annotating more than one file results
in a duplicate attribute error.) The FIDL style guide recommends
creating a file named overview.fidl
for this purpose.
On the library declaration, the @available
attribute requires the added
argument and allows the platform
argument. If the platform
is omitted, it
defaults to the first component of the library name. For example:
// Equivalent to `@available(platform="fuchsia", added=1)`.
@available(added=1)
library fuchsia.examples.docs;
Modifiers
FIDL versioning lets you add or remove modifiers at specific versions. After a
modifier, you can write arguments in parentheses the same way you would after
@available
. However, only the added
and removed
arguments are allowed.
Here is an example of changing an enum from strict to flexible:
type Color = strict(removed=2) flexible(added=2) enum {
RED = 1;
};
All modifiers support this syntax: strict
, flexible
, resource
, closed
,
ajar
, and open
. However, changing the strict
or flexible
modifier on a
two-way method without error syntax is not allowed.
When you target a set of versions, the compiler uses the latest modifiers. In the example above, the enum would be flexible if the set includes any version equal or greater to 2, even if it also includes 1.
Inheritance
The arguments to @available
flow from the library declaration to top-level
declarations, and from each top-level declaration to its members. For example,
if a table is added at version 5, there is no need to repeat this annotation on
its members because they could not exist prior to the table itself. Here is a
more complicated example of inheritance:
@available(added=2, deprecated=3)
open protocol Versioned {
// Equivalent to `@available(added=2, deprecated=3, removed=4)`.
@available(removed=4)
flexible Removed(table {
// Equivalent to `@available(added=3, deprecated=3, removed=4)`.
@available(added=3)
1: message string;
});
};
Deprecation
Deprecation is used to indicate that an element will be removed in the future.
When you deprecate an element, you should add a # Deprecation
section to the
doc comment with a detailed explanation, and a note
argument to the
@available
attribute with a brief instruction. For example:
open protocol Example {
/// (Description of the method.)
///
/// # Deprecation
///
/// (Detailed explanation of why the method is deprecated, the timeline for
/// removing it, and what should be used instead.)
@available(deprecated=5, removed=6, note="use Replacement")
flexible Deprecated();
@available(added=5)
flexible Replacement();
};
As of June 2024 deprecation has no impact in bindings. However, the FIDL team
plans to make it emit deprecation annotations in target
languages. For instance, the example above could produce #[deprecated = "Use
Replacement."]
in the Rust bindings.
Identity
FIDL versioning distinguishes between removing and replacing elements. To do this, it relies on a notion of API and ABI identity. The API identity of an element is its name. ABI identity depends on the kind of element:
- Bits/enum member: value, e.g. 5 in
VALUE = 5;
- Struct member: offset, e.g. 0 for first member
- Table/union member: ordinal, e.g. 5 in
5: name string;
- Protocol member: selector, e.g. "example/Foo.Bar" in
library example; protocol Foo { Bar(); };
Other elements, such as type declarations and protocols, have no ABI identity.
Replacing
The replaced
argument lets you change an element at a particular version by
writing a completely new definition. This is the only way to change certain
aspects of FIDL elements, including:
- The value of a constant
- The type of a struct, table, or union member
- The kind of a declaration, e.g. changing a struct to an alias
- The presence of
error
syntax on a method - Attributes on the element
To replace an element at version N
, annotate the old definition with
@available(replaced=N)
and the new definition with @available(added=N)
.
For example, here is how you change the value of a constant:
@available(replaced=5)
const MAX_NAME_LEN uint32 = 32;
@available(added=5)
const MAX_NAME_LEN uint32 = 64;
As another example, here is how you would change the type of a table field:
type Data = resource table {
@available(replaced=5)
1: name string:32;
@available(added=5)
1: name string:64;
};
The FIDL compiler verifies that for every @available(replaced=N)
element there
is a matching @available(added=N)
element with the same identity.
It also verifies that every @available(removed=N)
element does not have
such a replacement. This validation only applies to elements directly annotated,
not to elements that inherit the removed
or replaced
argument.
Renaming
To rename a member, replace it with a new definition and specify
the new name with the renamed
argument on the old definition. For example:
type User = table {
@available(replaced=2, renamed="first_name")
1: name string;
@available(added=2)
1: first_name string;
};
The renamed
argument is only allowed on members because the FIDL compiler
relies on their ABI identity to validate it. To rename a
declaration, simply remove the old definition in favor of a new one:
@available(deprecated=2, removed=3, note="renamed to Information")
type Info = table {};
@available(added=2)
type Information = table {};
After removal
Normally the renamed
argument is used with replaced=N
, but you can also use
it with removed=N
. This gives a new name to refer to the member after its
removal. How it works is based on the set of target
versions:
- If you only target versions less than
N
, the bindings will use the old name. - If you only target versions equal to or greater than
N
, the bindings won't include the member at all. - If you target a set containing versions less than
N
and containing versions greater than or equal toN
, the bindings will use the new name.
One reason to do this is to discourage new usage of an API while continuing to support its implementation. For example:
open protocol Door {
@available(removed=5, renamed="DeprecatedOpen")
flexible Open() -> ();
};
If the Door
server is implemented in a codebase that targets the version set
{4, 5}, then the method will be named DeprecatedOpen
, discouraging developers
from adding new uses of the method. If another codebase targets version 4 or
below, then the method will be named Open
. If it targets version 5, the method
will not appear at all.
Another reason to use this feature is to reuse a name for a new ABI. For
example, consider changing the method Open
to return an error:
open protocol Door2 {
@available(removed=5, renamed="DeprecatedOpen")
flexible Open() -> ();
@available(added=5)
@selector("NewOpen")
flexible Open() -> () error uint32;
};
We need to define a new method, since a client that doesn't expect an error will
close the channel if it receives an error response. However, we can keep using
the name Open
as long as we (1) use @selector
to give the new method a
different ABI identity and (2) use renamed
on the old definition,
allowing bindings for the version set {4, 5} to include both methods.
Referencing FIDL types
There are a variety of ways one FIDL element can reference another. For example:
const VALUE uint32 = 5;
const REFERENCES_VALUE uint32 = VALUE;
type Type = struct {};
type ReferencesType = table {
1: t Type;
};
alias ReferencesTypeAndValue = vector<Type>:VALUE;
When referencing elements, you must respect the @available
attributes. For
example, the following code is invalid because A
exists from version 1 onward,
but it tries to reference B
which only exists at version 2:
// Does not compile!
@available(added=1)
const A bool = B;
@available(added=2, removed=3)
const B bool = true;
Similarly, it is invalid for a non-deprecated element to reference a deprecated
element. For example, the following code is invalid at version 1 because A
references B
, but B
is deprecated while A
is not.
// Does not compile!
@available(deprecated=2)
const A bool = B;
@available(deprecated=1)
const B bool = true;
fidlc
command line
The FIDL compiler accepts the --available
flag to specify platform versions.
For example, assuming example.fidl
defines a library in the fuchsia
platform
with no dependencies, you can compile it at version 22 as follows:
fidlc --available fuchsia:22 --out example.json --files example.fidl
You can target multiple versions by separating them with commas, e.g.
--available fuchsia:19,22,23,NEXT,HEAD
.
If a library A
has a dependency on a library B
from a different platform,
you can specify versions for both platforms using the --available
flag twice.
However, A
must be compatible across its entire version history with the fixed
version chosen for B
.