Presentation: The AIDL build system — what we freeze, and how it is consumed - rdkcentral/rdk-halif-aidl GitHub Wiki
A consumer chooses a version to write code against. You cannot implement or call an interface without picking one.
| What it means | |
|---|---|
| Builds against | One chosen version. The code is written to that interface, so the choice is fixed at build time and changing it is a decision someone makes. |
| Runs against | That version, or a later minor or patch of it. Within one major the protocol is backwards-compatible. |
The build choice is fixed. The runtime match is a range.
| Requirement | Today | |
|---|---|---|
| 1 | A consumer builds against a version it chooses, whatever anyone else chooses | ✅ each build configuration pins its own versions manifest |
| 2 | Two consumers in one integration build against different versions of the same component | ✅ across separately built layers — see the limit on one process |
| 3 | A consumer names the version it wants, and the build resolves where that version lives | 🟡 today the consumer spells out the paths too |
| 4 | Client and vendor are isolated at runtime — each resolves its own libraries from its own rootfs location | ❌ install half works, load half does not |
| 5 | A released <component>/<version>/ is immutable after release |
❌ five commits since June edited build files inside frozen snapshots |
| 6 | The surface an engineer implements is documented, per released version | ❌ ships undocumented |
A client or a server implements against one specific version. The consumer states that version; the build resolves the location — sysroot, mount, directory nesting.
Integration cost, as promised to integrators: one line in a recipe, two in CMake.
Requirement 1 — a consumer builds against a version it chooses.
flowchart LR
subgraph MW["Middleware process"]
MWP["Bp — proxy for IVideoDecoder"]
MWS["Bn — stub for its listeners"]
end
subgraph VEN["Vendor HAL component"]
VENS["Bn — stub for IVideoDecoder"]
VENP["Bp — proxy for the listeners"]
end
MWP -- "calls the HAL" --> VENS
VENP -- "calls back" --> MWS
Both compiled from the same released snapshot — videodecoder/0.2.0.0.
Each side is a client of some interfaces and a server of others, so a binding set cannot be shipped in halves. A single HAL server exists per interface — and both layers run servers.
Requirement 2 — and the reason the client side has to be shared.
flowchart LR
P["player<br/>Bp copy"] --> S
R["recorder<br/>Bp copy"] --> S
D["diagnostics<br/>Bp copy"] --> S
S["Vendor HAL component<br/>Bn — one server<br/>self-contained binary"]
| Server side | Client side |
|---|---|
| One component, one server process. Nothing to share with anyone. | The proxy links into every process that calls the HAL. Static would mean a copy in each, and no central patching. |
Shared is the right answer on the client side, and it costs the server nothing — which is why
one .so per component version serves both.
Frozen interfaces, additive only. A version is never broken, so a newer server keeps serving every client built before it.
flowchart LR
C1["client built against V1"] -- "ok" --> S["vendor at V3<br/>V1 + V2 + V3 methods"]
C2["client built against V2"] -- "ok" --> S
C3["client built against V3"] -- "ok" --> S
The rule is ordering alone: server >= client.
There is no major to align, because nothing is ever removed or changed — only added. A client never sees a method disappear, so it never has to care how far ahead the vendor has gone.
Breaking changes are permitted, so the major has to match on both sides.
flowchart LR
C["client built against 0.2.2.0"]
C -. "unsupported — major not aligned" .-> A["vendor 0.1.x.x"]
C -. "unsupported — vendor behind client" .-> B["vendor 0.2.1.0"]
C -- "ok — exact" --> D["vendor 0.2.2.0"]
C -- "ok — vendor ahead" --> E["vendor 0.2.3.0"]
C -. "unsupported — major not aligned" .-> F["vendor 0.3.x.x"]
The rule is alignment plus ordering: same major and vendor >= client.
| Vendor ahead within the major | Supported. The client's methods are all still there. |
| Vendor behind the client | Unsupported. The client may call what the vendor does not implement. |
| Major not aligned, either direction | Unsupported runtime configuration. Rejected, not degraded. |
halcompat::isCompatible() applies whichever rule the era calls for, so a mismatch is
reported rather than discovered by crashing.
Ours today, against the model we are heading for.
| Ours today — era 0 | Android stable AIDL | |
|---|---|---|
| Breaking change | Allowed, with a major bump | Never, once frozen — additive only |
| Client ÷ vendor | Must share a major | Independent — any frozen version coexists |
| Versions built | Those the cohort links | Every frozen version, always |
| Supported range | Implied by the cohort | Declared in the VINTF compatibility matrix |
Android buys coexistence by giving up the right to break. We have not paid that price
yet — every component is still 0.x and breaking changes are the expected output of the
review cycle. Alignment is the cost of still being able to change the interfaces.
Requirements 1, 2 and 4 — the artefacts that make them true.
One shared library per component version, carrying both roles.
add_library(... SHARED ...) is what the snapshots build today, so
lib<comp>-v<ver>-cpp.so already is the default.
Bp and Bn are one translation unit |
A binding set is not separable, so one library serves the client and the server sides alike. |
A static .a for the server |
Possible, and not required. The .so is fine for both, and it is what already gets built. |
| The version is in the library name | Which is how several versions occupy one directory, and how the loader tells them apart. |
Android reaches the same place — libbinder_ndk is shared, and stub libraries carry the
version in the name.
Not today. The released cohort is uniform — every importer of a shared component agrees on its version:
Importers of common
|
All on |
|---|---|
| audiodecoder, audiomixer, audiosink, avclock, compositeinput, hdmicec, hdmiinput, hdmioutput, panel, planecontrol, videodecoder, videosink | [email protected] |
avbuffer, panel, planecontrol, videosink → videodecoder
|
[email protected] |
One version of each component exists in the cohort, so no process can hold two.
Neither of our isolation mechanisms is what protects this. Separate rootfs locations divide middleware from vendor, and major alignment governs client against server — both are across processes. This case is two components inside one process sharing a dependency.
Two versions of a component export identical symbols — the C++ namespace carries no version, and nothing is compiled hidden:
libvideodecoder-v0.2.0.0-cpp.so _ZN3com...12videodecoder13IVideoDecoder19getInterfaceVersionEv
libvideodecoder-v0.2.0.1-cpp.so _ZN3com...12videodecoder13IVideoDecoder19getInterfaceVersionEv
Load both and the first one wins, for everybody — no error:
| Link order | Caller asked for | Got |
|---|---|---|
0.2.0.1 first |
0.2.0.1 | 0.2.0.1 |
0.2.0.0 first |
0.2.0.1 | 0.2.0.0 |
This is the tension in requirement 2. What is delivered today is different versions across separately built layers. Different versions within one process is a further step, and the symbol model does not support it.
Ask the room: do we commit to a uniform cohort and state it as a constraint — or make symbols version-distinct, and pay for it in the generator?
Requirement 4 — and the one not yet met.
flowchart TB
subgraph NOW["Today — no RUNPATH"]
A1["MW binary"] --> X["one libbinder.so<br/>whichever the loader finds"]
A2["vendor binary"] --> X
end
subgraph FIX["With RUNPATH per artifact"]
B1["MW binary"] --> Y1["/mw/usr/lib<br/>its own libbinder"]
B2["vendor binary"] --> Y2["/vendor/usr/lib<br/>its own libbinder"]
end
Two builds of the same source, same library name, same symbols — separated by where they are installed and where they look.
-
The install half already works. Setting the prefix gives two complete trees — library,
headers and
servicemanager— under/mw/usrand/vendor/usr. -
The load half does not. No
RUNPATHis emitted anywhere, so resolution falls to the loader's default path.
Requirement 4, continued.
DT_RUNPATH is not transitively inherited. A middleware service carrying
RUNPATH=/mw/usr/lib does not help libbinder.so find its own liblog, libbase,
libcutils and libutils — each of those needs its own.
Why not DT_RPATH
|
And a standing rule |
|---|---|
It is inherited — but LD_LIBRARY_PATH overrides it, which with two libbinders present is worse than the problem it solves. |
Neither prefix may go in /etc/ld.so.conf.d/. If it does, the ldconfig cache order decides — unstable across images. |
Without this the failure is silent: both stacks load the same library, and nothing reports an error.
Fixed by linux_binder_idl PR #63, merged 22 September (#62).