RFC-0083: FIDL versioning | |
---|---|
Status | Accepted |
Areas |
|
Description | Provide a way to version FIDL elements and generate bindings at a particular version. |
Issues | |
Gerrit change | |
Authors | |
Reviewers | |
Date submitted (year-month-day) | 2021-02-12 |
Date reviewed (year-month-day) | 2021-04-05 |
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.
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 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.
Figure 1: API evolution before (left) and after (right) FIDL Versioning
Terminology
The terms API level and ABI revision are defined in RFC-0002: Platform Versioning, 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)
- 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
A FIDL property is a modifiable aspect of a FIDL element that is not itself a distinct element. This includes:
- Attributes
- The modifiers
strict
,flexible
, andresource
- 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
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
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, we instead use 2^64-2 for
HEAD
and 2^64-1 forLEGACY
.
Amendment (Apr 2024).
HEAD
andLEGACY
were redefined in RFC-0246.
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.
Syntax
An availability attribute has the following form,2 inspired by Swift's
available
attribute:
@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 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:
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
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 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
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, and the
behavior in bindings is as described in RFC-0058: Introduce a [Deprecated]
Attribute. If it is absent, we omit it from the JSON IR.
If a FIDL element is not used 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
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 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, 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 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
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 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
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
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 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.
- Use the
- Be careful when changing FIDL properties. For example, changing a type from strict to flexible, or from value to 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 thenote
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.
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 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. 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 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 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 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. 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. 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 for details.
We also need to remove the [Deprecated]
attribute from the FIDL
attributes page, since the availability attribute makes it obsolete.
The FIDL source compatibility documentation 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:
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:
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:
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 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. Reading that RFC is important to understanding the context and motivation behind this one. Its Prior art and references 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 much like the ones in this
proposal, and Objective-C uses similar API_AVAILABLE
attributes.
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
#[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 and 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.
-
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. ↩
-
This document uses the syntax introduced by RFC-0086: Updates to RFC-0050: FIDL Attributes Syntax. ↩
-
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. ↩