{% set rfcid = “RFC-0232” %} {% include “docs/contribute/governance/rfcs/_common/_rfc_header.md” %}
{# Fuchsia RFCs use templates to display various fields from _rfcs.yaml. View the #} {# fully rendered RFCs at https://fuchsia.dev/fuchsia-src/contribute/governance/rfcs #}
Note: This RFC is an amendment to RFC-0083: FIDL versioning, and it is dependent on RFC-0231: FIDL versioning replacement syntax.
When generating FIDL bindings today, we target LEGACY
in tree and a numbered API level out of tree. This document proposes to generalize and obsolete LEGACY
by providing a way to target multiple API levels at once. This is only intended to be used in tree initially, but we may find situations where will be useful out of tree.
With the original design of FIDL versioning, it was impossible to remove an element while retaining ABI support for it. For example, if a CL marked a method as removed=5
, it would also have to delete the implementation of that method. That is because we built the Fuchsia platform at HEAD
, and the method's server bindings would no longer exist at HEAD
since it is greater than 5.
To solve this, we amended RFC-0083 to introduce the LEGACY
version and the legacy
argument. The LEGACY
version is like HEAD
, except it re-adds removed elements if they are marked legacy=true
.
There are a few problems with LEGACY
:
It's a pseudo-version that carries no information on its own. Bindings for a frozen API level N
contain all APIs that are part of N
, but bindings for LEGACY
contain whatever happened to be marked removed
with legacy=true
at the time of the build (in addition to everything in HEAD
).
Legacy support is determined on a per-API basis, and changes over time. This makes it difficult to guarantee that a platform build actually supports a given older API level.
It is not possible to target a specific API level (i.e. other than HEAD
) while including legacy support.
It only solves a subset of compatibility challenges, where the Fuchsia platform is one side of the communication. There are protocols that are spoken between product components, where this is not the case.
It privileges the Fuchsia monorepo so that it becomes harder to split components out of the monolithic repository and release process.
Facilitator: abarth@google.com
Reviewers: hjfreyer@google.com, wilkinsonclay@google.com, ddorwin@google.com
Consulted: wez@google.com, sethladd@google.com
Socialization: I discussed this idea with the FIDL team and the Platform Versioning working group before writing the RFC.
We propose to allow targeting multiple API levels at once when generating FIDL bindings. For example, invoking fidlc with --available fuchsia:10,15
would target API levels 10 and 15, resulting in bindings that combine elements from both levels. If an element with a given name has different definitions under 10 and 15, we use the definition from level 15 because it is newer.
This obsoletes the LEGACY
version. When building the Fuchsia platform, instead of targeting LEGACY
bindings, we will target the set of supported API levels. This also eliminates the need to mark individual APIs with legacy=true
.
Remove the LEGACY
version, and the @available
attribute's legacy
argument, from the FIDL language.
Change fidlc's --available
command line argument syntax from <platform>:<target_version>
to <platform>:<target_versions>
where <target_versions>
is a comma-separated list of versions. Examples:
--available fuchsia:10
--available fuchsia:10,11
--available fuchsia:10,20,HEAD
The <target_versions>
list must be sorted and must not contain duplicates. This is to emphasize the fact that versions create a linear history, and later versions are treated preferentially.
The <target_version>
list determines a set of candidate elements:
An element marked @available(added=A)
is a candidate element if <target_versions>
intersects {v | v >= A}
.
An element marked @available(added=A, removed=R)
is a candidate element if <target_versions>
intersects {v | A <= v < R}
.
Note that this RFC is dependent on RFC-0231: FIDL versioning replacement syntax. For the purposes of determining candidates elements, replaced
is treated the same as removed
.
An element is included in bindings if (1) it is a candidate element, and (2) among all candidates with the same name, its added
version is greatest.
If an element marked @available(..., deprecated=D, ...)
is included in bindings by the rules above, it is considered deprecated in bindings if <target_versions>
intersects {v | v >= D}
. This has no impact on generated code today, but may in the future (https://fxbug.dev/42156877).
As before, the --available
flag can be used more than once for multiple platforms. There is no significant interaction between these two features (multiple platforms and multiple target versions).
As before, the success or failure of compilation MUST be independent of the --available
flag for the main library's platform. (It may be dependent on the --available
flag for a dependency in another platform.) For example, if compilation succeeds with --available fuchsia:15,16
, it is guaranteed to also succeed with --available fuchsia:10,100,HEAD
. Similarly, if the former fails, the latter is guaranteed to fail with the same set of errors.
When building the Fuchsia platform, replace --available fuchsia:LEGACY
with --available fuchsia:<target_versions>
where <target_versions>
includes all runtime supported API levels, the in-development API level, and HEAD
.
This design allows generating bindings for a valid FIDL library that target any arbitrary set of versions, regardless of how the library has evolved over time. This is a significant constraint, since FIDL versioning can represent any syntactically valid change. In particular, fidlc allows multiple elements with the same name to coexist as long as their version ranges do not overlap. When <target_versions>
would include multiple such elements, we only include the newest element. This supports three general patterns of evolution:
Lifecycle. An element is added
and possibly removed
. We include it in bindings when targeting any version within its lifecycle. Example:
@available(added=1, removed=5) flexible Method() -> ();
Replacement. An element is added
, and later replaced
with a different definition. Conceptually, this represents a single element changing over time, not two distinct elements. We assume the replacement was designed to be compatible with the original element, and only include the replacement element in bindings. Example:
@available(added=1, replaced=5) flexible Method() -> (); @available(added=5) flexible Method() -> () error uint32;
Name reuse. After an element is removed
, its name can be reused for a new element added
later. This is like replacement, but the two elements are conceptually distinct, and there is a gap between their lifecycles. We assume the newer element is preferred, and only include it in bindings. Example:
@available(added=1, removed=5) flexible Method(); @available(added=10) flexible Method() -> ();
Note that when an element name is reused in this way, references to it cannot span the gap between the two definitions. For example, this would not compile:
@available(added=1, removed=5) type Args = struct {}; @available(added=10) type Args = table {}; @available(added=2) protocol Foo { Method(Args); // ERROR: 'Method' exists at versions 5 to 10, but 'Args' does not };
Consider the following FIDL library:
@available(added=1) library foo; @available(replaced=2) type E = strict enum { V = 1; }; // E1 @available(added=2) type E = flexible enum { V = 1; }; // E2 @available(added=3, removed=6) open protocol P { @available(removed=4) flexible M() -> (); // M1 @available(added=5) flexible M(table {}) -> (); // M2 };
Here is what's included in bindings when selecting a single version:
--available | E1 | E2 | P | M1 | M2 |
---|---|---|---|---|---|
foo:1 | ✔︎ | ||||
foo:2 | ✔︎ | ||||
foo:3 | ✔︎ | ✔︎ | ✔︎ | ||
foo:4 | ✔︎ | ✔︎ | |||
foo:5 | ✔︎ | ✔︎ | ✔︎ | ||
foo:6 | ✔︎ | ||||
foo:HEAD | ✔︎ |
And here is what's included when selecting multiple versions:
--available | E1 | E2 | P | M1 | M2 |
---|---|---|---|---|---|
foo:1,2 | ✔︎ | ||||
foo:1,HEAD | ✔︎ | ||||
foo:1,3 | ✔︎ | ✔︎ | ✔︎ | ||
foo:1,2,3 | ✔︎ | ✔︎ | ✔︎ | ||
foo:3,6 | ✔︎ | ✔︎ | ✔︎ | ||
foo:3,HEAD | ✔︎ | ✔︎ | ✔︎ | ||
foo:2,4,6 | ✔︎ | ✔︎ | ✔︎ | ||
foo:1,3,5 | ✔︎ | ✔︎ | ✔︎ | ||
foo:1,2,3,4,5,6,HEAD | ✔︎ | ✔︎ | ✔︎ |
Implement RFC-0231: FIDL versioning replacement syntax.
Implement the new --available
functionality in fidlc. Also change the “available” property in the JSON IR to use an array of strings for versions.
Change all existing legacy
arguments to line up with the new system (i.e. false
if removed before our minimum supported API level, and true
if removed on or after it). If there is a large discrepancy, consider Alternative: Override mechanism.
Change the in-tree platform build to generate bindings targeting all supported API levels, the in-development API level, and HEAD
.
Remove all legacy
arguments in FIDL files.
Remove LEGACY
support from fidlc.
This proposal has no impact on performance.
This proposal makes FIDL versioning easier to use correctly, since there is no need to worry about the legacy
argument anymore.
This proposal helps to achieve ABI backward compatibility, since it removes the burden of choosing legacy=true
from individual FIDL library authors. It also gives more credence to our stated set of “supported API levels”, since those API levels are directly used to generate bindings for the platform. (Of course, to truly be confident they are supported we need tests as well.)
This proposal has no impact on security.
This proposal has no impact on privacy.
This following files must be updated to test the new behavior:
The following documentation pages must be updated:
This proposal could be seen as reducing the incentive described in RFC-0002: Platform Versioning to migrate off deprecated APIs, since you can access both new and old APIs by targeting multiple levels. However, this is already possible today with LEGACY
. Just as petals should not target LEGACY
today, they should not misuse this new feature.
Also, since petals use fidlc via the SDK rather than invoking it directly, we can mitigate this with restrictions in the SDK build rules. For example, they could assert that the target version string does not contain a comma.
Instead of allowing an arbitrary set of versions, we could require a range specified by two endpoints. I rejected this alternative for a few reasons:
If we decide to increase the cadence of API levels, it might be easier to only maintain long-term support for some fraction of them. This would lead to a set of versions with gaps, not a range.
We might want to support an individual old component that targets API level N
without recopmiling it. If everything else has already moved off API levels N
through M
, we could have a gap of {N+1, ..., M}
.
Nothing we've built for platform versioning so far has assumed a contiguous range of supported API levels. For example, version_history.json contains a list of API levels, not a range.
Using ranges instead of sets would not make the fidlc implementation easier. It might make it slightly more efficient, but this is unlikely to matter in practice. There are many lower hanging fruit to optimize should fidlc performance ever become a problem.
One drawback of this proposal is that it could be difficult to update all code in fuchsia.git atomically when dropping support for an API level. To split such changes into multiple steps, we might want a more granular way to control what gets included in bindings. There are a few options for that:
Override <target_versions>
in individual fidl
GN targets.
Add an @available
argument unsupported=true
which excludes the element even if it would normally be included. This is similar to legacy
, but would only be used temporarily (ideally).
Change the --available
argument to accept a JSON file which, in addition to <target_versions>
, can give a list of fully qualified element names to include or exclude.
I rejected this alternative because it‘s not clear we will need this mechanism. Instead, we should first try making changes in a single CL. If that doesn’t work, we should try using conditional compilation to stage changes, so that implementations are only included before dropping support for the API level. If that doesn't work, we can revisit the override mechanisms above.
We could also mitigate this by increasing the cadence of API levels, which would result in fewer removals per API level. However, this would have many other implications for platform versioning and is out of scope in this proposal.
legacy
true by defaultSee RFC-0233: FIDL legacy by default.
This alternative improves on the status quo. With false
as the default, forgetting to add legacy=true
can cause ABI breakage. With true
as the default, forgetting to add legacy=false
can only result in a fidlc compile error or unused APIs in bindings, a much less severe problem.
However, this is only a minor change and does not address all the problems raised in this RFC. The legacy
state would still be controlled per API, leading to inconsistent runtime support for a given API level, and making it hard to determine whether a particular build fully supports an API level.
The Android SDK allows specifying compileSdkVersion
and minSdkVersion
. See Android API Levels and the <uses-sdk>
documentation.