blob: b4f5705166a75492a3aa8d547599095636e7eb67 [file] [log] [blame] [view]
{% set rfcid = "RFC-0083" %}
{% include "docs/contribute/governance/rfcs/_common/_rfc_header.md" %}
# {{ rfc.name }}: {{ rfc.title }}
<!-- SET the `rfcid` VAR ABOVE. DO NOT EDIT ANYTHING ELSE ABOVE THIS LINE. -->
## Summary
This document proposes a way to annotate FIDL elements with versions and a
mechanism to generate bindings at a given version. This decouples API evolution
from its adoption, making it easier for library authors to make changes while
providing stability to end-developers. This lays the groundwork for FIDL's role
in [RFC-0002: Platform Versioning][rfc-0002].
## Motivation
While FIDL provides many affordances for ABI compatibility during changes, in
practice evolving APIs is difficult. In the Fuchsia SDK, making a FIDL change
that is ABI-compatible but not API-compatible requires a carefully coordinated
soft transition to avoid breaking compilation downstream. When something does
break, we usually have to revert the change in Fuchsia. As usage of SDK
libraries increases, so does the difficulty of making these changes.
FIDL Versioning addresses this, allowing FIDL library authors and consumers to
move forward at their own pace. When a library author adds, removes, or modifies
an API, the change is released in a new API level. Applications targeting the
old API level see no change in the bindings until they adopt the new API level.
In addition to providing stability, this lets end-developers migrate to new APIs
one component at a time, since target API levels are specified per component.
[Figure 1](#fig-1) illustrates a breaking API change. Without versioning, it
breaks an application and leads to a revert. With versioning, the application
simply stays pinned to the old API level. Of course, the same problems will
arise when attempting to bump the application's pinned API level from 12 to 13.
But these can be fixed asynchronously, without reverting the original change in
Fuchsia and grinding the project to a halt.
![API evolution diagram with text description above]
(resources/0083_fidl_versioning/fig_1.png){:#fig-1}
**Figure 1: API evolution before (left) and after (right) FIDL Versioning**
## Terminology {#terminology}
The terms _API level_ and _ABI revision_ are defined in [RFC-0002: Platform
Versioning][rfc-0002], with the following change:
> _Amendment to [RFC-0002]_. A Fuchsia _API level_ is an unsigned, 63-bit
> integer. In other words, it is 64 bits but the high bit must be zero.
A _FIDL element_ is a discrete part of a FIDL library that affects the generated
bindings. This includes:
* FIDL libraries themselves
* Constant, enum, bits, struct, table, union, protocol, and service declarations
* Aliases and new types (from [RFC-0052: Type Aliasing and New Types][rfc-0052])
* Members of enums, bits, structs, tables, unions, and services (including
`reserved` members of tables and unions)
* Methods and `compose` stanzas in protocols
* Request and response parameters in methods
Note: When one protocol composes another, we consider the latter's _original_
methods and the former's _composed methods_ to be distinct FIDL elements. This
is counter to the [compositional model], which considers them identical.
A _FIDL property_ is a modifiable aspect of a FIDL element that is not itself a
distinct element. This includes:
* [Attributes][attrs]
* The modifiers `strict`, `flexible`, and `resource`
* Values of constants, enum members, and bits members
* Default values of struct members
* Type constraints (on members, parameters, and type aliases)
* Method kinds (one-way, two-way, event)
* Method error syntax (its presence and type)
This leaves only a few things that are neither FIDL elements nor properties:
* Individual `.fidl` files
* Imports of other FIDL libraries
* FIDL-only `using` aliases, which [RFC-0052] removes from the language
* Experimental [`resource_definition`] declarations
* Comments, including documentation comments
## Design
The design described in this document provides a general-purpose facility for
versioning FIDL elements. Its primary use case is to version FIDL libraries in
the Fuchsia Platform by API level.
### Scope {#scope}
This design introduces versioning as a concept in the FIDL language, giving a
temporal dimension to FIDL libraries. It specifies the syntax and semantics of
versioning attributes, including how they interact with other aspects of FIDL
such as parent-child and use-define relationships. It establishes how versions
are provided as input when generating FIDL bindings.
This design does _not_ propose a package manager for FIDL. Topics such as
version resolution algorithms, package distribution, and dependency conflicts
are out of scope. That being said, a system addressing those problems should be
able to reuse the tools provided by this design.
This proposal does not address runtime behavior: it focuses on API, not on ABI.
The topic of _protocol evolution_ is therefore out of scope. This includes
questions such as, "How can a FIDL server support multiple ABI revisions?" There
are a variety of protocol evolution strategies FIDL and Component Manager could
adopt in the future. This proposal paves the way to protocol evolution by
introducing the concept of versions in FIDL, but it goes no further than this.
The ability to represent a transition under this design has no bearing on
whether that transition is safe or compatible. On the contrary, a versioned FIDL
library can represent almost any sequence of syntactically valid changes.
### Formalism {#formalism}
A _platform identifier_ is a label that gives context to versions. Platform
identifiers must be a valid FIDL library name element, i.e. as of this writing
match the regex `[a-z][a-z0-9_]*`.
A _version identifier_ is an unsigned 64-bit integer between 1 and 2^63-1
(inclusive) or equal to 2^64-1. The latter version identifier is known as `HEAD`
and is treated specially.
> *Amendment (Oct 2022).* To support [legacy methods](#legacy), we instead use
> 2^64-2 for `HEAD` and 2^64-1 for `LEGACY`.
Version identifiers are totally ordered by an "is newer than" relationship.
Version _X_ is newer than version _Y_ when _X_ > _Y_.
The _availability_ of a FIDL element with respect to a platform refers to the
version when the element was _introduced_, and optionally the versions when it
was _deprecated_ and _removed_. Deprecation and removal must be newer than
introduction.[^1] If both are supplied, removal must be newer than deprecation.
A FIDL element is _versioned under_ a platform if it has an availability with
respect to that platform. It is _versioned_ if versioned under any platform.
A FIDL element is _available_ with respect to a platform version if the version
is newer than or equal to the element's introduction, but older than its
deprecation and removal (if any). It is _deprecated_ if the version is newer
than or equal to the element's deprecation, but older than its removal (if any).
It is _present_ if it is available or deprecated. Otherwise, it is _absent_.
A _version selection_ is an assignment of versions to a set of platforms. For
example, one could select version 2 of `red` and version `HEAD` of `blue`.
A FIDL element is _available_ with respect to a version selection if it is
available with respect to all platforms. It is _deprecated_ if it is present
with respect to all platforms, and deprecated with respect to one or more
platforms. Otherwise, it is _absent_.
Note: The primary use case for this design is to version FIDL libraries in the
Fuchsia Platform. In this case, all libraries are versioned with respect to the
`fuchsia` platform identifier, and version identifiers correspond to API levels.
### Syntax
An _availability attribute_ has the following form,[^2] inspired by [Swift's
`available` attribute][swift-attr]:
@available(added=<V>, deprecated=<V>, removed=<V>)
Each `<V>` is a version identifier. The `added`, `deprecated`, and `removed`
fields denote the introduction, deprecation, and removal of the element,
respectively. They are all optional, but at least one must be supplied.
On libraries, the `added` field must be provided (`deprecated` and `removed` are
optional). There is also an optional field `platform` specifying a platform
identifier. All version identifiers in the library refer to versions of this
platform. For example:
@available(platform="red", added=2)
library colors.red.auth;
When omitted, it defaults to the first component of the library name:
@available(added=HEAD) // implies platform="blue"
library blue.auth;
With the `deprecated` field, an additional `note` field can be given for
inclusion in warning messages. For example:
@available(added=12, deprecated=34, note="Use X instead")
The availability attribute makes the `[Deprecated]` attribute from [RFC-0058:
Introduce a `[Deprecated]` Attribute][rfc-0058] obsolete.
### Versioning elements
FIDL elements are versioned using availability attributes. Each FIDL element can
have at most one availability attribute, and this can only be done in versioned
libraries. In other words, if any FIDL element in a library is annotated, then
the library must be annotated as well.
Each file in a FIDL library has its own library declaration, but they all
represent the same FIDL element: the library. This is consistent with the [FIDL
style guide][style]:
> The division of a library into files has no technical impact on consumers of
> the library. ... Divide libraries into files to maximize readability.
Therefore, only one library declaration in a library can have an availability
attribute. Doc comments are restricted in the same way, so it makes sense to
choose the same file to specify the library's availability and its doc comment.
### Versioning properties {#versioning-properties}
FIDL properties cannot be versioned directly. To change a property, you must
_swap_ the element it belongs to. This means duplicating the element, removing
the old copy and introducing the new copy at the same version. For example, to
change a string bound at version 12:
@available(removed=12)
string:50 info;
@available(added=12)
string:100 info;
Or to change an enum from `strict` to `flexible`:
@available(removed=12)
strict enum Color { ... };
@available(added=12)
flexible enum Color { ... };
All FIDL elements except libraries can be swapped. Naming conflicts do not arise
because the availabilities do not overlap.
This proposal does not preclude a future syntax for applying availability
attributes directly to FIDL properties. If such a syntax were introduced, it
could only support `added` and `removed`, as there is no interpretation for
`deprecated` that makes sense across all FIDL properties.
### Inheritance
FIDL elements form directed acyclic graphs, with child elements inheriting
their availability from parent elements.
Top level declarations inherit from the library. Members of enums, bits,
structs, tables, unions, and services inherit from the enclosing declaration.
Request/response parameters inherit from their method. Methods and `compose`
stanzas inherit from the enclosing protocol. Composed methods inherit from the
original method and the `compose` stanza. When protocol composition is not used,
the graph is a tree.
If a child element has an availability attribute, it overrides the inherited
availability. In doing so, it must be neither redundant nor contradictory:
introduction versions can only be made newer, and deprecation and removal
versions can only be made older.
For a composed method, if both parents are versioned under the same platform,
its availability is the intersection of its parents' availabilities (newest
introduction, oldest deprecation, and oldest removal). If they are versioned
under different platforms, the composed method inherits two separate
availabilities. In this case, the [definition](#formalism) of "available with
respect to a version selection" becomes relevant. In both cases, the deprecation
`note` is combined from both parents.
Here is the general case of composition within a platform:
library foo;
protocol Def { @available(added=A, deprecated=B, removed=C) Go(); };
protocol Use { @available(added=D, deprecated=E, removed=F) compose Def; };
The original method `foo/Def.Go` is introduced at `A`, deprecated at `B`, and
removed at `C`. The composed method `foo/Use.Go` is introduced at `max(A,D)`,
deprecated at `min(B,E)`, and removed at `min(C,F)`. This means all composed
methods are bound by the `compose` stanza's availability, but some can have a
narrower availability if `Def` introduces them after the `compose` introduction,
or deprecates/removes them before the `compose` deprecation/removal.
### Use validation {#use-validation}
Certain FIDL elements are related in that one _uses_ the other. A
struct/table/union member or request/response parameter uses a FIDL element if
the element occurs in its type; a method, if the element occurs in its error
type; a const or enum/bits member, if the element occurs in its value; a struct
member, if the element occurs in its default value. Some examples:
const uint32 X = 10;
const uint32 Y = X; // Y uses X
table Entry {};
protocol Device {};
resource struct Info {
vector<Entry>:X entries; // entries uses Entry and X
request<Device> req; // req uses Device
uint32 val = Y; // val uses Y
Info? next; // next uses Info
};
Given a version selection, fidlc produces an error if:
* a present element uses an absent element; or
* an available element uses a deprecated element.
### Lifecycle semantics
Given a version selection, if a FIDL element is available, it is emitted as
usual. If it is deprecated, we [denote this in the JSON IR](#json-ir), and the
behavior in bindings is as described in [RFC-0058: Introduce a `[Deprecated]`
Attribute][rfc-0058]. If it is absent, we omit it from the JSON IR.
If a FIDL element is not [used](#use-validation) by any other, annotating it
with `@available(removed=<N>)` is equivalent to deleting it from the `.fidl`
file, except that using the `removed` attribute maintains historical accuracy,
whereas deleting the element does not. This provides a way to avoid `.fidl`
files becoming bloated and unreadable as changes accumulate.
### Purpose of `HEAD`
The `HEAD` version identifier represents the bleeding edge of development.
Clients are free to program against `HEAD` bindings, but they should not expect
them to be stable. For example, suppose you download `red.fidl`, where the
highest version identifiers used in annotations are 12 and `HEAD`. If you
download a newer copy of `red.fidl`, it is reasonable to expect the API at
version 12 to be identical, i.e. that the authors have not altered history. But
the `HEAD` API might be completely different.
This feature provides continuity when adopting FIDL Versioning. To depend on the
`HEAD` bindings of a versioned library is the same as depending on the bindings
of an unversioned library.
It also makes FIDL changes easier in collaborative projects. When authoring a
CL, looking up the current version is tedious and race-prone, especially if it
changes during code review. Instead, contributors can simply use `HEAD`, and
project owners can replace it with a specific version later.
### Legacy support {#legacy}
> *Amendment (Oct 2022).* This section was added after the RFC was accepted.
When an API is removed using `@available(removed=<N>)`, it no longer appears in
the generated bindings for versions _N_ and above. This makes it hard to build a
Fuchsia system image that supports multiple API levels. If the system image is
built against _N_-1 bindings, it cannot provide implementations for methods
added at _N_. If it is built against _N_ bindings, it cannot provide
implementations for methods removed at _N_.
To solve this problem, we introduce a new version called `LEGACY` that acts the
same as `HEAD` but also includes legacy methods. A _legacy method_ is a method
marked `@available(removed=<N>, legacy=true)`. This uses a new boolean argument
called `legacy` which is false by default, and only allowed when `removed` is
present. For example:
@available(added=1)
library example;
protocol Foo {
@available(removed=2) // implies legacy=false
NotLegacy();
@available(removed=2, legacy=true)
Legacy();
};
Here are the methods included in that example's bindings when targeting
different versions:
| Target version | Methods included |
|:--------------:|:---------------------:|
| 1 | `NotLegacy`, `Legacy` |
| 2 | |
| `HEAD` | |
| `LEGACY` | `Legacy` |
As a matter of policy, all methods in the Fuchsia platform should retain legacy
support when they are removed. Once the Fuchsia platform drops support for all
API levels before the method's removal, it is safe to remove `legacy=true` and
the method's implementation.
When the Fuchsia platform acts as a client instead of a server, legacy methods
allow the platform to continue calling the method for those targeting old API
levels. For those targeting newer API levels that do not expect it, the method
must be marked `flexible` so that the calls can be ignored. See [RFC-0138:
Handling unknown interactions][rfc-0138] for more details.
The `legacy` argument can be used on any FIDL element, not just on methods. For
example, if you are removing a type along with the method that uses it, that
type must be marked `legacy=true` as well. This is just a consequence of [use
validation](#use-validation), not a new rule.
As another example, consider a table used in a request. When removing one of
its fields, you might wish to use `legacy=true` so that the server can continue
supporting clients that set the field. On the other hand, if ignoring the field
is sufficient to preserve ABI, there is no need for legacy support. Similarly,
for a table used in a response, it is only necessary to use `legacy=true` when
removing a field if setting that field is required to preserve ABI for old
clients.
Legacy support should never be used when [swapping](#versioning-properties) an
element because the availabilities represent change, not removal. If you were to
do so, it would cause an error:
protocol Foo {
@available(removed=2, legacy=true)
Bar();
@available(added=2)
Bar();
}
Since the first `Bar` gets added back at `LEGACY`, and the second `Bar` is never
removed, they both exist at `LEGACY` and fidlc will emit an error like it
already does for same-named elements with overlapping availabilities.
### JSON IR {#json-ir}
To represent deprecation in the IR, we add two fields:
deprecated: <bool>, // required
deprecation_note: <string>, // optional
These are added to the following [JSON IR Schema][schema] definitions:
#/definitions/bits
#/definitions/bits-member
#/definitions/const
#/definitions/enum
#/definitions/enum-member
#/definitions/interface
#/definitions/interface-method
#/definitions/service
#/definitions/service-member
#/definitions/struct
#/definitions/struct-member
#/definitions/table
#/definitions/table-member
#/definitions/union
#/definitions/union-member
#/definitions/type_alias
Note that the IR does not represent the deprecation of a library. It still has
an effect via inheritance, as well as warnings described in the next section.
### Command-line interface {#command-line-interface}
To specify a version selection, fidlc will accept `--available <P>:<V>` where
`<P>` is a platform identifier and `<V>` is a version identifier. The flag can
be given multiple times for distinct platform identifiers. For example:
fidlc --json out.json --available red:2 --available blue:HEAD
--files red.fidl --files blue.fidl
If the version selection is missing a platform or has an unused platform
(compared to the platforms the given libraries are versioned under), fidlc
produces an error. If any library is deprecated/absent with respect to the
version selection, fidlc produces a warning/error.[^3]
## Policy {#policy}
> *Note (Oct 2022)*. This section sketched out an initial policy, and is no
> longer up to date. In particular, most new changes should be added at the
> current in-development API level, not at `HEAD`. See [FIDL API compatibility
> testing][api-compat-testing] for details.
FIDL Versioning makes it possible to evolve APIs without breaking applications,
but it does not guarantee it. To that end, we adopt the following policies,
specifically for the Fuchsia Platform:
* Annotate all new changes as occurring at `HEAD`.
* Do not alter the history of a FIDL library. The only exception is the process
of _deleting old FIDL elements_, described below.
* Deprecate FIDL elements before removing them, except when
[swapping](#versioning-properties) to change a property.
* When deprecating an element:
* Use the `note` field to tell developers what to use instead.
* Write a `# Deprecation` section in the doc comment giving a more detailed
explanation and communicating the deprecation timeline.
* Be careful when changing [FIDL properties](#versioning-properties). For
example, changing a type from [strict to flexible][strict-vs-flexible], or
from [value to resource][value-vs-resource], can have a significant API
impact. The API Council should judge these changes on a case-by-case basis.
These policies will be enforced as follows:
* All FIDL changes in the SDK will continue to require API Council approval.
* `fidl-lint` should check that deprecated elements have the `note` field set
and a `# Deprecation` section in their doc comment.
* In the future, there should be a CQ job that enforces the other policies
(altering history, deprecating before removal, and API/ABI incompatible
changes) based on [FIDL API summaries][RFC-0076].
There are also two new processes whose details we defer to a later RFC:
* _Releasing new API levels._ This will likely happen on a fixed schedule, where
some or all of the changes made since the last API level are released in a new
API level by replacing occurrences of `HEAD` with the new level.
* _Deleting old FIDL elements._ Once enough time has passed, elements marked as
removed can be deleted from `.fidl` files. An element can only be deleted if
it is not referenced anywhere, so this process will likely involve deleting
all elements older than a particular API level on a fixed schedule.
We can build tools to make both of these processes easier, using the same tree
visitor approach as fidl-format.
## Implementation
This design can mostly be implemented in fidlc. Parsing the `@available` syntax
is dependent on [another RFC][rfc-0086] to change FIDL's annotation syntax. The
semantics will likely be implemented behind an experimental flag at first.
When fidlc compiles a library, even though it produces JSON IR at a single
version, it should validate all possible versions simultaneously. It should
_not_ do so by generating and checking each version sequentially. Instead, it
should temporally decompose elements into (name, version range) tuples. This
process is analogous to [converting an NFA to a DFA][nfa-dfa]. For example:
type MyTable = table {
@available(added=2)
1: name string;
@available(added=HEAD)
2: age uint32;
};
This would decompose as follows (using pseudo syntax to demonstrate):
type «MyTable, [0,1]» = table {};
type «MyTable, [2,HEAD)» = table { 1: name «string, [0,HEAD]»; }
type «MyTable, HEAD» = table { 1: name «string, [0,HEAD]»; 2: age «uint32, [0,HEAD]» };
Just before emitting IR, fidlc will prune the declarations to only include those
requested in the version selection.
> _Open problem_. The temporal decomposition approach is difficult to generalize
> when FIDL libraries versioned under different platforms are compiled together.
> Since this is not needed for our primary use case (versioning the Fuchsia
> Platform by API level), we can defer this problem and initially have fidlc
> only allow one `--available` flag.
The `HEAD` version identifier can be implemented as a context-specific constant,
similar to the [`MAX` constant][max-bound] that is allowed as a length
constraint on strings and vectors.
There is also some implementation work outside fidlc. First, fidldoc needs to
take versioning into account. For example, if an element is deprecated, the
documentation should indicate this prominently. It could also provide an API
level dropdown for viewing historical documentation. Second, fidlgen backends
needs to use the `"deprecated"` field in the JSON IR. For example, fidlgen_rust
could translate it to the `#[deprecated]` Rust attribute. See [RFC-0058:
Introduce a `[Deprecated]` Attribute][rfc-0058] for examples in other languages.
Before libraries in the SDK start using the annotations, we will need to add
`--available fuchsia:HEAD` to the GN templates for building FIDL bindings. This
is based on the assumption that all in-tree code will use `HEAD` bindings. When
we have a Platform Versioning proposal for C++, it might be necessary to build
in-tree code against other versions of FIDL bindings for testing.
In petal build systems, we will add `fuchsia_api_level` declarations and wire
them up to the `--available` flag. This will need to be coordinated with [fidlc
CLI changes](#command-line-interface) by at first accepting and ignoring the
`--available` flag before requiring it.
## Performance
This proposal has no impact on runtime performance. It affects build performance
to the extent that fidlc must do more work, but FIDL compilation has never been
a significant factor in Fuchsia build times.
## Security considerations
This proposal should have a positive impact on security, since versioning makes
it easier to migrate to new FIDL APIs with better security properties. This
should outweigh the negative impact of increasing the attack surface by having
to support old ABI revisions.
This proposal does not provide a mechanism for hiding ABIs based on an
application's target ABI revision, [as suggested in
RFC-0002][rfc-0002-security]. While this could enhance security, it would be
better designed as part of a comprehensive RFC on protocol evolution.
## Privacy considerations
This proposal should have a positive impact on privacy, since versioning makes
it easier to migrate to new FIDL APIs with better privacy properties.
## Testing
We currently test the FIDL toolchain with a combination of unit tests and golden
tests. Unit testing is mostly used for fidlc internals. Golden testing works by
compiling a suite of `.fidl` files and ensuring the resulting artifacts (JSON IR
and all bindings) are identical to previously vetted _golden files_.
FIDL Versioning will take a similar approach. It will use unit tests for small
pieces of logic in fidlc. For example, there will be a test ensuring that
compilation fails with an appropriate error message when a table member's
annotation says it was introduced before the table itself. It will also use
golden tests, but not by extending the existing golden testing framework.
Generating artifacts for libraries at every version would bloat the golden files
and make it hard to verify correctness. Instead, this project will have its own
set of `.fidl` files with golden _diffs_ of the JSON IR at each version. This
should make it easy to verify that versioning behaves as expected.
This won't make testing harder for implementers of platform APIs: tests will be
written against `HEAD`, the same way we currently don't run tests against FIDL
files from old git revisions. Nor does it make testing harder for SDK users:
they will test against a single version of the platform the same way they
currently test using a single release of the SDK.
## Documentation
The `@available` syntax will be documented in the [FIDL language
specification][language]. More documentation will be needed once there is a
process in place for releasing new API levels. For instance, we need to teach
library authors to use `@available(added=HEAD)` whenever adding a new API
element. With proper tooling, there should be no danger of forgetting to do
this. See the [Policy section](#policy) for details.
We also need to remove the `[Deprecated]` attribute from the [FIDL
attributes][attrs] page, since the availability attribute makes it obsolete.
The [FIDL source compatibility documentation][source-compat] should be updated
either to show FIDL changes using availability attributes, or to show how to
apply different kinds of FIDL diffs when using versioning. The documentation
should also describe how FIDL versioning interacts with transitions in general.
With versioning, changing a FIDL element is just as easy whether it is used
out-of-tree or not. This should reduce the need for some kinds of soft
transitions. But it does not eliminate all multi-step transitions; it just
removes the constraint of a single shared timeline when coordinating them.
## Drawbacks, alternatives, and unknowns
### What are the costs of implementing this proposal?
This proposal adds complexity to FIDL (the language) and to fidlc. It will make
it more tedious for library authors to make simple, safe changes, but easier for
them to make other types of changes (e.g. adding a member to a strict enum) with
confidence.
### Alternative: Use old SDKs
FIDL Versioning allows applications to stay pinned to old API levels while
continuing to roll new SDKs. But why not simply use an old SDK, rendering this
whole proposal unnecessary? There are a couple reasons:
* With an up-to-date SDK, users get the latest copies of everything else, such
as the FIDL toolchain.
* Target API levels are specified per component. Using a different SDK for each
component is complicated and impractical.
### Alternative: Changelog file
Instead of availability attributes, a separate changelog could record the
history of a FIDL library. One approach would be a set of textual diffs going
back from the each `.fidl` file to its original. This would simplify many
things, such as the difficulty of versioning FIDL properties. It would be
impractical to validate all versions of a library at once, as in the proposed
design. However, this is perhaps less necessary as this alternative eliminates
the problem of altering history by accident. But it would make it harder to
answer questions such as, "When was this element introduced?" It would
essentially duplicate git history, with the main difference being that history
is preserved when creating a downloadable SDK.
Textual diffs would be difficult to maintain if we make changes to FIDL syntax
in the future. Another variant of the changelog design would be defining a new
format to record changes to a FIDL library, and the version when they occurred.
This design is compatible with validating all versions at once, since fidlc
could read the changelog and produce a temporally decomposed AST the same as if
the information had come from attributes. However, it would require more
tooling. For example, we might want developers to edit `.fidl` files as they do
today, and run a tool to append to the changelog file before committing.
### Alternative: Per-library versions
An alternative design is to have a separate version for each FIDL library. This
would lead to a mapping from API levels to versions of every FIDL library in the
SDK. For example, API level 42 might represent `fuchsia.auth` v1.2,
`fuchsia.device` v5.7, and so on.
This approach has advantages for those concerned with an individual library.
Each version would be meaningful with respect to that library, and you could
estimate how much a library has evolved from its current version number. In
contrast, with per-platform versions there can be large gaps between versions
where something changes in a library.
But it raises lots of questions. Does having per-library versions mean that SDK
libraries must track the versions of other SDK libraries they depend on? Can SDK
consumers mix and match different versions of SDK libraries? Answering either
with "yes" adds a lot of complexity to FIDL Versioning. How do we know if a
given set of versions works together? How do we avoid compiling multiple copies
of the same library's bindings together? If the answer to both is "no", then
per-library versioning seems like needless indirection, making it appear that
versioning happens at the library level when it does not.
### Alternative: Asymmetric deprecation
RFC-0002 states in its [Lifecycle section][rfc-0002-lifecycle]:
> The element might be deprecated. Components that target older ABI revisions
> can still use the element when running on newer platform releases. However,
> end-developers that target a newer API level can no longer use the element.
It goes on to say [what this means for FIDL][rfc-0002-fidl]:
> When a protocol element (e.g., a field in a table or a message in protocol) is
> deprecated at a given API level, we would ideally like components that target
> that API level to be able to receive messages containing that protocol element
> but would like to prevent those components from sending messages that contain
> that protocol element.
FIDL Versioning departs from this behavior, and so it is included here as an
alternative. Preventing end-developers from using FIDL elements at a given API
level, while allowing code in the Fuchsia platform to support it at runtime, is
difficult. As stated, it relies on the incorrect assumption that the Fuchsia
Platform always acts as a server and the SDK consumer always acts as a client.
There are cases where the roles are reversed, or even ambiguous. We could
distinguish these by introducing attributes such as `@platform_implemented` and
`@user_implemented`. That helps with methods, but asymmetric behavior for types
and members of types (called _type elements_ below) is harder to solve.
One way to achieve asymmetric deprecation of type elements is to generate stubs
preventing their use. For example, a deprecated table field could appear in
bindings as a value of type `FidlDeprecated`, which generates typechecking
errors when used. Code in the Fuchsia Platform could continue supporting the
deprecated element via a new fidlgen flag `--allow-deprecated` that generates
code as if nothing is deprecated. But there are two problems with this approach.
First, it makes it difficult to eliminate use of deprecated elements within
Fuchsia, since they do not appear as deprecated. Second, it would be very easy
for end developers to use the flag as well. This negates the [desired
incentive][rfc-0002-dynamics]:
> This approach incentivizes developers to migrate away from deprecated
> interfaces by coupling access to new APIs to performing those migrations.
> Specifically, to gain access to a newly introduced API, the developer must
> change their target API level, which requires them to migrate off any
> interfaces that were deprecated in that API level.
Namely, with `--allow-deprecated`, developers can gain access to newly
introduced APIs without migrating off deprecated ones simply by using the flag.
Another approach for type elements would be to generate errors at runtime. For
example, if a table field is deprecated, bindings could produce an error during
encoding if the field is present (but leave decoding unchanged). However,
runtime behavior is [out of scope](#scope) for this proposal.
In summary, asymmetric deprecation is too subtle and complex to be included in
this proposal. These challenges could possibly be worked out in a future RFC if
the benefit of asymmetric deprecation is worth the complexity.
### Alternative: Full history IR
Under this proposal, version information only exists prior to the JSON IR. Once
the IR has been produced, we are working with a fixed version. This is
sufficient for generating bindings, but it is less useful for tools like fidldoc
which might want to use version information. Rather than these tools parsing
`.fidl` files, or inferring lifecycles by comparing the JSON IR at multiple
versions, an alternative would be to introduce a new mode of JSON IR that
includes all history and availability information. This is different from simply
including availability attributes in the IR because it would mean including
elements that are marked as removed at the latest version.
There are two problems with this alternative. First, it is undesirable that some
JSON IR files should have a slightly different schema and purpose than others.
It might be better to design an entirely new format, but this has downsides too.
Second, it is difficult to determine what this full history IR should look like
without an idea of the UI fidldoc should present. For example, what would it
show for a type whose `resource` modifier has been added and removed ten times?
This sort of question, and the representation used, would be better addressed in
a separate RFC.
## Prior art and references
This proposal is part of the overall plan laid out in [RFC-0002: Platform
Versioning][rfc-0002]. Reading that RFC is important to understanding the
context and motivation behind this one. Its [Prior art and
references][rfc-0002-prior-art] section focuses on other operating systems:
Android, Windows, and macOS/iOS. Here, we focus on other programming language
and IDLs, and their approaches to API versioning.
### Swift, Objective-C
Swift uses [`@available` attributes][swift-attr] much like the ones in this
proposal, and Objective-C uses similar [`API_AVAILABLE` attributes][objc-attr].
They are limited to a hardcoded list of Apple platforms such as macOS and iOS.
They can also use the `swift` platform to control availability based on the
Swift language version being used during compilation. Versions are specified as
one, two, or three numbers separated by dots, following [semver] semantics. Both
languages provide a similar syntax for checking platform versions at runtime.
### Rust
Rust annotates its standard library with [stability attributes][rust-attr]
`#[stable]`, `#[unstable]`, and `#[rustc_deprecated]`. Each unstable element is
linked to a GitHub issue, and can only be used by developers who opt in with the
corresponding `#[feature]` attribute. Stable attributes indicate the Rust
version at which the element was stabilized. However, this is just for
documentation; it does not control visibility.
### Protobuf, gRPC
[Protocol Buffers] do not provide tools for versioning. Instead, they place a
greater focus on forward and backward compatibility than FIDL does. For example,
there are no structs (only _messages_, which are like FIDL tables), no strict
types (all types have flexible behavior), and no exhaustive matching supported
on enumerations (as of proto3).
Google Cloud APIs use Protocol Buffers with [gRPC], and provide guidelines on
[versioning][gcp-versioning] and [compatibility][gcp-compatibility]. The
versioning strategy is based on conventions, not features built into the system.
APIs encode their major version number at the end of the protobuf package, and
include it in URI paths. In this way services can support multiple major
versions at once, and clients receive backwards-compatible updates in place,
i.e. without taking action to migrate.
[^1]: During implementation, this rule was relaxed to allow introduction and
deprecation to coincide. This makes it possible to manually decompose FIDL
declarations at any version boundary by [swapping](#versioning-properties).
[^2]: This document uses the syntax introduced by [RFC-0086: Updates to
RFC-0050: FIDL Attributes Syntax][rfc-0086].
[^3]: During implementation, these rules were omitted to simplify integration
with the build system. For the version selection, fidlc uses `HEAD` by
default and ignores unused platforms. For library declarations, the
availability has no effect apart from inheritance, so an absent library is
equivalent to an empty one, and there is no warning for deprecation.
<!-- xrefs -->
[rfc-0002]: /docs/contribute/governance/rfcs/0002_platform_versioning.md
[rfc-0002-lifecycle]: /docs/contribute/governance/rfcs/0002_platform_versioning.md#lifecycle
[rfc-0002-fidl]: /docs/contribute/governance/rfcs/0002_platform_versioning.md#fidl
[rfc-0002-dynamics]: /docs/contribute/governance/rfcs/0002_platform_versioning.md#dynamics
[rfc-0002-security]: /docs/contribute/governance/rfcs/0002_platform_versioning.md#security-considerations
[rfc-0002-prior-art]: /docs/contribute/governance/rfcs/0002_platform_versioning.md#prior-art-and-references
[rfc-0076]: /docs/contribute/governance/rfcs/0076_fidl_api_summaries.md
[rfc-0058]: /docs/contribute/governance/rfcs/0058_deprecated_attribute.md
[rfc-0052]: /docs/contribute/governance/rfcs/0052_type_aliasing_named_types.md
[rfc-0086]: /docs/contribute/governance/rfcs/0086_rfc_0050_attributes.md
[rfc-0138]: /docs/contribute/governance/rfcs/0138_handling_unknown_interactions.md
[language]: /docs/reference/fidl/language/language.md
[attrs]: /docs/reference/fidl/language/attributes.md
[swift-attr]: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID583
[objc-attr]: https://developer.apple.com/documentation/swift/objective-c_and_c_code_customization/marking_api_availability_in_objective-c
[rust-attr]: https://rustc-dev-guide.rust-lang.org/stability.html
[schema]: /tools/fidl/fidlc/schema.json
[source-compat]: https://fuchsia.googlesource.com/fuchsia/+/f8c48f630f43202e3e5c6d7395459ed0fc5e4c6d/src/tests/fidl/source_compatibility
[`resource_definition`]: https://fuchsia.googlesource.com/fuchsia/+/f8c48f630f43202e3e5c6d7395459ed0fc5e4c6d/zircon/vdso/zx_common.fidl#93
[semver]: https://semver.org/
[Protocol Buffers]: https://developers.google.com/protocol-buffers
[gRPC]: https://grpc.io/
[gcp-versioning]: https://cloud.google.com/apis/design/versioning
[gcp-compatibility]: https://cloud.google.com/apis/design/compatibility
[nfa-dfa]: https://en.wikipedia.org/wiki/Powerset_construction
[compositional model]: /docs/contribute/governance/rfcs/0023_compositional_model_protocols.md#compositional_model
[strict-vs-flexible]: /docs/reference/fidl/language/language.md#strict-vs-flexible
[value-vs-resource]: /docs/reference/fidl/language/language.md#value-vs-resource
[max-bound]: https://fuchsia-review.googlesource.com/c/fuchsia/+/325737
[style]: /docs/development/languages/fidl/guides/style.md#files
[api-compat-testing]: /docs/development/testing/ctf/fidl_api_compatibility_testing.md