Enum and Union
Beyond interfaces and parcelables, AIDL has enums — named integer constants — and unions — a value that is one of several types at a time. Both map onto Rust, though the enum mapping is not the one you would guess.
Enum Types
AIDL enums are backed by a specific integer type, declared using the @Backing annotation. Unlike Rust's native enums, AIDL enums map to newtype structs wrapping the backing integer. This design preserves wire compatibility and allows values outside the defined set, which is important for forward compatibility in IPC.
Defining Enums in AIDL
The @Backing(type=...) annotation specifies the underlying integer type. It is optional — when omitted, the backing type defaults to byte. Here are examples for each supported backing type:
Byte-backed enum:
@Backing(type="byte")
enum ByteEnum {
FOO = 1,
BAR = 2,
BAZ,
}
Int-backed enum:
@Backing(type="int")
enum IntEnum {
FOO = 1000,
BAR = 2000,
BAZ,
}
Long-backed enum:
@Backing(type="long")
enum LongEnum {
FOO = 100000000000,
BAR = 200000000000,
BAZ,
}
When a value is omitted (as with BAZ above), it is automatically assigned the previous value plus one. So BAZ would be 3, 2001, and 200000000001 respectively.
Backing Type Mapping
The AIDL backing type determines the Rust integer type used inside the generated newtype struct:
| AIDL Backing Type | Rust Type |
|---|---|
byte | i8 |
int | i32 |
long | i64 |
Using Enums in Rust
Enum values are accessed as associated constants on the generated struct. The generated type derives Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, and Hash, plus serialization traits automatically.
#![allow(unused)] fn main() { // Access enum values as associated constants let e = ByteEnum::FOO; let result = service.RepeatByteEnum(e)?; assert_eq!(result, ByteEnum::FOO); }
Enums work naturally with arrays and vectors:
#![allow(unused)] fn main() { // Enums can be used in arrays let input = [ByteEnum::FOO, ByteEnum::BAR, ByteEnum::BAZ]; let mut repeated = vec![]; let reversed = service.ReverseByteEnum(&input, &mut repeated)?; }
Each generated enum type provides an enum_values() method that returns a fixed-size array ([Self; N]) of all defined values, which is useful for iteration and validation:
#![allow(unused)] fn main() { // enum_values() returns all defined values let all_values = ByteEnum::enum_values(); }
Enums in Service Interfaces
Enums are commonly used as parameters and return types in AIDL interfaces:
interface ITestService {
ByteEnum RepeatByteEnum(ByteEnum token);
IntEnum RepeatIntEnum(IntEnum token);
LongEnum RepeatLongEnum(LongEnum token);
ByteEnum[] ReverseByteEnum(in ByteEnum[] input, out ByteEnum[] repeated);
}
The generated Rust trait methods use the enum types directly, providing compile-time type safety across the IPC boundary.
Union Types
AIDL unions represent a tagged value that holds exactly one of several possible fields at a time. They map to Rust enum types, which are a natural fit since Rust enums are sum types with variants.
Defining Unions in AIDL
Here is a union definition from the rsbinder test suite:
@RustDerive(Clone=true, PartialEq=true)
union Union {
int[] ns = {};
int n;
int m;
@utf8InCpp String s;
@nullable IBinder ibinder;
@utf8InCpp List<String> ss;
ByteEnum be;
const @utf8InCpp String S1 = "a string constant in union";
}
Key points about union definitions:
- The first field is the default. When a union is default-constructed, it takes the value of the first field. In this example, the default is
nsinitialized to an empty array{}. A first field without an initializer takes its type's RustDefault, as in AOSP's Rust backend; for an enum that is the backing value0, not the first enumerator. - Fields can have different types, including primitives, strings, arrays, other AIDL types, and even binder references.
- Constants can be defined inside unions, independent of the union's variants.
@RustDeriveis recommended so the generated Rust type supportsCloneandPartialEq.
Using Unions in Rust
The AIDL union generates a Rust enum. Because AIDL types are organized into modules, the union type and its enum variants live inside a module named after the union. Variants are accessed as Union::Union::VariantName(...). A variant's name is its field's name with only the first letter uppercased, as in AOSP's Rust backend: ns becomes Ns, nullable_iface becomes Nullable_iface, and URL stays URL. Two fields whose names differ only in the first letter's case (foo and Foo) are therefore rejected, as AOSP rejects them.
#![allow(unused)] fn main() { // Default value is the first field assert_eq!(Union::Union::default(), Union::Union::Ns(vec![])); // Creating union variants let u1 = Union::Union::N(42); let u2 = Union::Union::S("hello".into()); let u3 = Union::Union::Be(ByteEnum::FOO); }
Constants defined inside a union are accessed directly on the module, not through a variant:
#![allow(unused)] fn main() { // Constants defined in the union let s = Union::S1; // "a string constant in union" // Using a constant as a union variant value let u = Union::Union::S(Union::S1.to_string()); }
Union Tags
Each union has an associated Tag type that identifies which variant is currently active. (Like AIDL enums, it is generated as a newtype struct whose variants are exposed as associated constants such as Union::Tag::n. It is backed by i32, or by i8 in a @FixedSize union, as in AOSP.) Tags are useful when you need to inspect or communicate which field a union holds without extracting the value itself.
#![allow(unused)] fn main() { let result = service.GetUnionTags(&[ Union::Union::N(0), Union::Union::Ns(vec![]), ])?; assert_eq!(result, vec![Union::Tag::n, Union::Tag::ns]); }
Tags can also be used in match expressions when implementing service logic. Here is an example from the test service implementation:
#![allow(unused)] fn main() { fn GetUnionTags( &self, input: &[Union::Union], ) -> Result<Vec<Union::Tag>> { Ok(input.iter().map(|u| match u { Union::Union::Ns(_) => Union::Tag::ns, Union::Union::N(_) => Union::Tag::n, Union::Union::M(_) => Union::Tag::m, Union::Union::S(_) => Union::Tag::s, Union::Union::Ibinder(_) => Union::Tag::ibinder, Union::Union::Ss(_) => Union::Tag::ss, Union::Union::Be(_) => Union::Tag::be, }).collect()) } }
Unions Containing Enums (EnumUnion)
Unions can contain enum types as fields and specify default values using enum constants:
@RustDerive(Clone=true, PartialEq=true)
union EnumUnion {
IntEnum intEnum = IntEnum.FOO;
LongEnum longEnum;
/** @deprecated do not use this */
int deprecatedField;
}
In this example, the default value is the first field (intEnum) initialized to IntEnum.FOO. In Rust:
#![allow(unused)] fn main() { assert_eq!(EnumUnion::default(), EnumUnion::IntEnum(IntEnum::FOO)); }
A /** @deprecated note */ block on a field becomes #[deprecated = "note"] on the generated variant (#[deprecated] when there is no note). The generated module allows the lint for its own plumbing, so only your code's uses warn — which fails a -D warnings build until you migrate or allow it.
Nested Unions
Unions can also be nested inside other unions:
union UnionInUnion {
EnumUnion first;
int second;
}
This allows building complex tagged-value hierarchies that are fully type-safe on the Rust side.
Tips
- Specify
@Backingexplicitly. It is optional — the default isbyte— but stating it makes the wire format unambiguous to a reader. - Handle values you never declared. An AIDL enum is an integer on the wire, so a peer built against a newer definition can send one the current enum does not name. That is the point of the newtype mapping; do not assume the set is closed.
- Tags keep the AIDL field names, so they are lowercase:
Union::Tag::ns, notUnion::Tag::Ns.
Further Reading
- Android AIDL documentation -- the upstream reference for AIDL syntax and semantics
- AIDL annotation reference -- details on
@Backing,@RustDerive, and other annotations - The rsbinder test suite at
tests/aidl/andtests/src/test_client.rscontains comprehensive examples of enum and union usage