AIDL Annotations
Annotations modify what the code generator emits from an .aidl file — trait derivation, backing types, nullability, interface stability. Some that AOSP enforces are recognized here but not checked; each section below says which.
If you have not read AIDL Data Types yet, start there: annotations adjust those mappings rather than replacing them.
@RustDerive
The @RustDerive annotation tells the code generator to add Rust derive attributes to parcelable and union types. Without this annotation, generated types receive only the minimum set of derives needed for serialization.
@RustDerive(Clone=true, PartialEq=true)
parcelable Point {
int x;
int y;
}
This generates a Rust struct with both Clone and PartialEq derived:
#![allow(unused)] fn main() { #[derive(Debug, Clone, PartialEq)] pub struct Point { pub x: i32, pub y: i32, } }
Available Derives
| Derive | Description |
|---|---|
Clone | Enables cloning of the type |
PartialEq | Enables equality comparison |
Copy | Enables bitwise copy (fixed-size types only) |
These are the most common choices, but the list is not exhaustive: as in AOSP, the generator passes through any Name=true identifier as a derive, so other derivable traits (Eq, Hash, PartialOrd, Ord, ...) work the same way.
Why Clone Is Not Derived by Default
Generated types do not derive Clone by default. This is intentional because some AIDL types contain fields that cannot be cloned:
ParcelFileDescriptorwraps anOwnedFd, which represents sole ownership of a file descriptor. Cloning it would require duplicating the file descriptor at the OS level, which is not a simple bitwise copy.ParcelableHoldercontains aMutex, which cannot be cloned.
If your parcelable contains only primitive fields and cloneable types, add @RustDerive(Clone=true) explicitly. If the type contains a ParcelFileDescriptor or ParcelableHolder field, attempting to derive Clone will produce a compile error.
Copy Derivation for Fixed-Size Types
For parcelables that contain only primitive fields, you can derive Copy in addition to Clone. This is typically combined with the @FixedSize annotation:
@RustDerive(Clone=true, Copy=true, PartialEq=true)
@FixedSize
parcelable IntParcelable {
int value;
}
The Copy derive is only valid when all fields are Copy types. Using it on a parcelable that contains String, arrays, or other heap-allocated types will result in a compile error.
@Backing
The @Backing annotation specifies the underlying integer type for an AIDL enum. This controls both the wire format and the generated Rust type.
@Backing(type="byte")
enum Priority {
LOW = 0,
MEDIUM = 1,
HIGH = 2,
}
The generated Rust code uses a newtype struct wrapping the corresponding integer type, with the enum values exposed as associated constants:
#![allow(unused)] fn main() { pub mod Priority { #![allow(non_upper_case_globals)] // Generated by rsbinder::declare_binder_enum! — produces a // newtype struct, not a type alias, so values stay type-safe // and the enum carries its full set of derived traits. pub struct Priority(pub i8); impl Priority { pub const LOW: Self = Self(0); pub const MEDIUM: Self = Self(1); pub const HIGH: Self = Self(2); } } }
The generated type derives Debug, Default, Copy, Clone,
PartialOrd, Ord, PartialEq, Eq, and Hash, and provides an
enum_values() associated function returning a [Self; N] of all
defined variants. See Enum and Union for
the full code-generation details.
Supported Backing Types
| AIDL Backing | Rust Type | Size |
|---|---|---|
"byte" | i8 | 1 byte |
"int" | i32 | 4 bytes |
"long" | i64 | 8 bytes |
If no @Backing annotation is specified, the default backing type is "byte".
Choose the smallest backing type that fits your range of values. Enum values are serialized to the Binder transaction as their backing integer type, so a smaller backing type produces a more compact wire format.
@nullable
The @nullable annotation marks a type as optional, indicating that the value may be absent. In Rust, this maps to Option<T>.
Basic Usage
Apply @nullable to method parameters, return values, or struct fields:
interface IUserService {
@nullable String getName();
void setValues(in @nullable int[] values);
}
The generated Rust signatures use Option:
#![allow(unused)] fn main() { fn getName(&self) -> rsbinder::BinderResult<Option<String>>; fn setValues(&self, values: Option<&[i32]>) -> rsbinder::BinderResult<()>; }
When None is passed over a Binder transaction, a null marker is written to the parcel. The receiving side deserializes it as None without allocating any data.
Heap-Allocated Nullable: @nullable(heap=true)
AOSP AIDL uses @nullable(heap=true) to mark fields of recursive or
self-referential parcelables, so that the C++ and Java backends know to
allocate the inner value on the heap:
parcelable RecursiveList {
int value;
@nullable(heap=true) RecursiveList next;
}
The Rust backend generates:
#![allow(unused)] fn main() { pub struct RecursiveList { pub value: i32, pub next: Option<Box<RecursiveList>>, } }
The Box indirection is necessary because without it, RecursiveList would contain itself directly, making the type infinitely large.
As in AOSP's Rust backend, a heap=true field is Option<Box<T>> wherever it
is, on a cycle or not, and a method argument or return keeps its usual type.
heap=true is accepted only on a parcelable or union type, and heap is the
annotation's only parameter; anything else is an error, as in AOSP.
rsbinder also boxes a bare @nullable field that closes a reference cycle — a
cycle of any length, not just a direct self-reference — because Rust can
represent it. A path through an interface handle or a Vec element already
keeps the type finite and closes no cycle. AOSP's aidl rejects a bare
@nullable on a cycle, so rsbinder-aidl prints a cargo:warning for it; write
heap=true if the same AIDL must also compile under the AOSP toolchain.
For non-recursive optional fields, plain @nullable is sufficient and avoids the extra heap allocation.
@utf8InCpp
The @utf8InCpp annotation exists in Android AIDL to specify UTF-8 encoding for strings in the C++ backend, where the default encoding is UTF-16. In rsbinder, this annotation has no effect because Rust strings are always UTF-8.
interface ITextService {
@utf8InCpp String getData();
@utf8InCpp List<String> getNames();
}
Both String and @utf8InCpp String produce identical Rust type mappings:
| Direction | Rust Type |
|---|---|
Input (in) | &str |
| Output / Return | String |
You may encounter this annotation in AIDL files that were originally written for Android's C++ backend. It is safe to keep or remove it when targeting rsbinder; the generated Rust code is identical either way.
@Descriptor
The @Descriptor annotation overrides the interface descriptor string that identifies an interface on the Binder wire protocol. This is useful when renaming an interface while maintaining backward compatibility with existing clients or services.
Every Binder interface has a descriptor string derived from its fully qualified name (e.g., android.aidl.tests.IOldName). When you rename an interface, the descriptor changes, breaking compatibility. The @Descriptor annotation lets you decouple the source name from the wire descriptor.
Consider an interface that was originally named IOldName:
// IOldName.aidl
interface IOldName {
String RealName();
}
You can create a new interface INewName that uses the same descriptor:
// INewName.aidl
@Descriptor(value="android.aidl.tests.IOldName")
interface INewName {
String RealName();
}
Because both interfaces share the same descriptor, they are interchangeable at the Binder level:
#![allow(unused)] fn main() { // A service registered as IOldName can be used as INewName let new_from_old = old_service .as_binder() .into_interface::<dyn INewName::INewName>(); assert!(new_from_old.is_ok()); }
This is particularly useful during interface migrations where you want to rename types in your codebase without requiring all clients and services to update simultaneously.
@VintfStability
The @VintfStability annotation marks a type or interface as part of the Vendor Interface (VINTF). VINTF-stable types are subject to stricter compatibility rules to ensure that vendor and system partitions can be updated independently.
@VintfStability
parcelable VintfData {
int value;
}
Stability Rules
In the upstream Android toolchain, VINTF-stable types are meant to contain only other VINTF-stable types so that their serialization format stays stable across independent system/vendor updates — critical for framework↔HAL compatibility.
rsbinder's generator stamps the generated type or interface with Stability::Vintf and enforces the rule at compile time: a @VintfStability declaration that references a type which is not VINTF-stable is an error. AOSP enforces the same rule through aidl_interface { stability: "vintf" }; rsbinder has no such build mode and checks the references instead, which rejects the same contracts. A @RustOnlyStableParcelable is exempt, as AOSP exempts a stable-API parcelable, and a non-VINTF type may still use a VINTF one.
A declaration nested inside a @VintfStability type is VINTF-stable too, matching AOSP's scoped lookup. At run time, ParcelableHolder::set_parcelable on a VINTF holder returns BadValue for a payload whose stability does not include VINTF.
@FixedSize
The @FixedSize annotation indicates that a parcelable has a fixed serialization size, meaning its wire format is always the same number of bytes regardless of the field values.
@FixedSize
parcelable FixedPoint {
int x;
int y;
}
Constraints
A @FixedSize parcelable or union may only contain:
- Primitive types (
boolean,byte,char,int,long,float,double) - Enums
- Other
@FixedSizeparcelables and unions - Fixed-size arrays (
int[4]) of the above
and may not contain String, IBinder, ParcelFileDescriptor, ParcelableHolder, an interface, a variable-length array (T[]), a List<T>, or a @nullable type. @FixedSize is not inherited by nested declarations.
rsbinder-aidl rejects a field that breaks these rules, porting AOSP's CanBeFixedSize. The annotation changes the generated code in one place: a @FixedSize union's implicit Tag is backed by byte (Tag(pub i8)) instead of int, as AOSP's UnionTagGenerater declares it, so a Tag[] goes on the wire as byte[], and such a union may have at most 128 fields. The layout and wire format of every field are otherwise unchanged.
Relationship with @RustDerive(Copy=true)
Copy is derived purely from @RustDerive(Copy=true); @FixedSize is not a prerequisite for it in rsbinder. Conceptually you should still only add Copy to types with a fixed layout, so pairing the two documents intent:
@RustDerive(Clone=true, Copy=true, PartialEq=true)
@FixedSize
parcelable Coordinate {
double latitude;
double longitude;
}
@EnforcePermission
@EnforcePermission declares the Android permission(s) a method requires.
The generated on_transact arm checks the caller against
PermissionManagerService before your handler runs and returns
EX_SECURITY if the check fails — so the handler body needs no
authorization code:
interface IExample {
@EnforcePermission("android.permission.INTERNET")
void doNetworking();
@EnforcePermission(allOf = {"android.permission.A", "android.permission.B"})
void needsBoth();
@EnforcePermission(anyOf = {"android.permission.A", "android.permission.B"})
void needsEither();
}
The companion documentation-only annotations @PermissionManuallyEnforced
and @RequiresNoPermission are recognized and emit no runtime check (they
exist so every method can declare its permission posture).
Kernel-only. Android permissions are a kernel-binder concept. Over the RPC transport an
@EnforcePermissionmethod is denied (EX_SECURITY) unconditionally — granting would mean granting root to an anonymous peer. For RPC authorization use the transport-native mechanisms, or inject aPermissionAuthorityto back the check with your own policy. See Security & Authorization.
Summary
| Annotation | Applies To | Rust Effect |
|---|---|---|
@RustDerive | parcelable, union | Adds derive attributes (Clone, Copy, PartialEq) |
@Backing | enum | Sets the backing integer type (i8, i32, i64) |
@nullable | field, param, return | Maps to Option<T> |
@nullable(heap=true) | field | heap=true is ignored; rsbinder boxes a field its own cycle analysis finds recursive |
@utf8InCpp | String | No effect in Rust (strings are always UTF-8) |
@Descriptor | interface | Overrides the wire descriptor string |
@VintfStability | parcelable, interface, type parameter | Stamps Stability::Vintf; every referenced type must be VINTF-stable (checked). On a type parameter (Foo<@VintfStability T>): the argument must be a @VintfStability declaration |
@FixedSize | parcelable, union, type parameter | Fields checked to be fixed-size; no field layout change, but a union's Tag is i8-backed (Copy comes from @RustDerive(Copy=true)). On a type parameter (MQDescriptor<@FixedSize T, Flavor>): the argument must itself be fixed-size |
@deprecated (javadoc) | any declaration, method, field, constant, enumerator | Emits #[deprecated] / #[deprecated = "note"] from the first /** … @deprecated … */ among the comments before the item (AOSP FindDeprecated, android-14.0.0_r50+). A comment written between the item's annotations, type and name is not read |
@EnforcePermission | interface method | Generates a PermissionManagerService check (kernel-only; denied over RPC) |
When writing AIDL files for rsbinder, the most commonly used annotations are @RustDerive (for ergonomic Rust types), @Backing (for enums), and @nullable (for optional values). The remaining annotations are important for interoperability with Android or for specific use cases like recursive types and interface migration.