Linux aggressively isolates processes to maintain security and stability. When you build a custom system service, it runs in its own dedicated process. Client applications cannot directly access your service's memory or call its methods. You face a fundamental OS restriction that prevents basic communication.
Android bridges this boundary using the Android Interface Definition Language (AIDL). AIDL provides a strict, language-agnostic contract between a client and a service. You define the capabilities of your service in a simple text file.
The Android build system parses your AIDL file and automatically generates the complex Java or C++ boilerplate required for Inter-Process Communication (IPC). This generated code handles the low-level details of moving data across process boundaries. To the client application, making a remote procedure call feels exactly like invoking a local method.
The following flowchart illustrates how the build system transforms your interface definition into runtime components. You will see how a single file splits into distinct client and service pieces. Notice that the proxy and stub classes live in completely separate memory spaces.
During compilation, the build system generates two separate classes from your AIDL file. A proxy class lives in the client process and forwards method calls to the Binder driver. Meanwhile, the stub class lives in the service process, receives those calls from Binder, and triggers your actual implementation. But how do we actually write this contract?
Defining the Method Signatures
You cannot expose a raw Java or C++ object across a process boundary. The operating system needs explicitly defined methods that specify exactly what data moves in which direction. Without this strict definition, the Binder driver would not know how to pack and unpack your variables.
We construct this contract by writing a primary AIDL interface file. This document lists the exact capabilities your service offers to the rest of the OS. The syntax looks similar to a standard Java interface but includes specific directional tags.
package android.os;
import android.os.MyCustomConfig;
import android.os.IMyCustomCallback;
/**
* Interface for interacting with the custom hardware service.
* {@hide}
*/
interface IMyCustomService {
int getStatus();
void applyConfiguration(in MyCustomConfig config);
void registerCallback(in IMyCustomCallback callback);
}
Notice the in tag on the parameters. You must explicitly tag complex method parameters as in, out, or inout. This directional tag instructs the Binder driver on how to optimize memory copying. The in tag means data flows only from the client to the service. An out tag dictates that the service populates the object to send back to the client.
Developers often include the {@hide} annotation in their interface comments. When building a system service strictly for internal platform components, this marker prevents the interface from compiling into the public Android SDK. Third-party apps compiled against public APIs will not even know your service exists. With the interface secured, we must now consider how to pass more than just basic primitives.
Moving Complex Data Across Boundaries
Standard primitives like integers and strings cross the Binder boundary effortlessly. However, real services usually require passing complex configuration objects or structured data. Passing a dozen individual primitives as method parameters quickly becomes unmaintainable.
We handle this requirement by defining Parcelable types directly in AIDL. Historically, developers had to write tedious manual serialization code in Java or C++ to flatten objects into a Binder Parcel. Modern AOSP development eliminates that manual work entirely.
package android.os;
/** {@hide} */
parcelable MyCustomConfig {
int timeoutMs;
boolean enableLogging;
String profileName;
}
The build system automatically generates the classes required to flatten this structure and reconstruct it on the receiving end. You simply import MyCustomConfig into your primary interface and use it as a parameter type. This keeps your definitions clean and prevents manual serialization bugs. But passing data in one direction is only half the battle.
Preventing Deadlocks with Asynchronous Callbacks
System services frequently need to push asynchronous events back to client apps. The problem is that Binder calls are inherently synchronous. If your system service calls a method on a client app, the service process blocks until the client finishes processing. A frozen or malicious client app could permanently lock up your core system service.
We prevent this vulnerability by defining callback interfaces with asynchronous behavior. You achieve this protection using the oneway keyword. Applying this keyword changes how the Binder driver handles the transaction.
package android.os;
/** {@hide} */
oneway interface IMyCustomCallback {
void onStateChanged(int newState);
}
When a service invokes a method on a oneway interface, the Binder driver dispatches the transaction and returns immediately. The calling process never waits for the client to respond. Consequently, the system service can broadcast state changes without worrying about the performance characteristics of the apps receiving those updates.
Interview Note: Knowing when to use
onewaydemonstrates senior-level understanding of system stability. Never allow a critical system process to block on a call to an untrusted app process.
This pattern protects the overall OS. We now have a secure, efficient contract that dictates how data moves between processes safely. The next step is writing the actual code that implements this contract on the device.