FIDL versioning

This document describes FIDL's API versioning features. For guidance on how to evolve Fuchsia 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.

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 8 as follows:

fidlc --available fuchsia:8 --out example.json --files example.fidl

You can target multiple versions by separating them with commas, e.g. --available fuchsia:7,8,9.

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.

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:

ArgumentTypeNote
platformstringOnly allowed on library
addeduint64Integer, NEXT, or HEAD
deprecateduint64Integer, NEXT, or HEAD
removeduint64Integer, NEXT, or HEAD
replaceduint64Integer, NEXT, or HEAD
notestringGoes with deprecated
renamedstringGoes with removed or replaced; 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 and replaced arguments are mutually exclusive.
  • Arguments must respect added <= deprecated < removed, or added <= deprecated < replaced.
  • The added, deprecated, removed, and replaced arguments inherit from the parent element if unspecified.

For example:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="arguments" %}

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:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="library" %}

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:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="inheritance" %}

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:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="deprecation" %}

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
  • Other attributes on the element
  • Modifiers such as strict, flexible, and resource

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 an enum from strict to flexible:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="replaced" %}

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:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="renamed" %}

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:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="rename_declaration" %}

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 to N, 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:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="discourage_use" %}

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:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="reuse_name" %}

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.

References

There are a variety of ways one FIDL element can reference another. For example:

{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/versioning.test.fidl" region_tag="references" %}

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;