AIDL编译器
AIDL 编译不是“把接口文本替换成 Stub/Proxy”。Android 17 的编译链先由 Soong 收集源文件和 import 根目录,再由 system/tools/aidl 解析 AST、加载依赖、执行语言和稳定性校验,最后根据 后端生成 Java、C++、NDK 或 Rust 文件。稳定接口还会同时参与 API dump、hash 和版本兼容检查。
本文面向已经读过 lunch目标与编译变体 和 ADB安装与权限管理 的读者。需要理解 Android.bp、Ninja action、Binder 接口和基本 .aidl 语法;不展开 Binder 驱动调度、Parcel 二进制布局和 HAL 迁移。读完后,读者应能从一个 aidl_interface 的 source 模块追到 aidl 命令、解释一次失败发生在哪个阶段,并判断修改接口是否需要更新 frozen API。
1. 编译边界
一次 AIDL 编译同时有三个 owner:Soong 拥有输入图和命令行,AIDL 编译器拥有 AST 与诊断,生成 后端拥有目标语言文件。生成文件的消费者是 Java/C++/Rust 模块,而不是编译器本身。
“编译成功”只表示当前 action 生成了目标语言输出,不表示接口已经稳定、兼容历史版本,也不表示 服务端已经实现了每个方法。稳定性由 --structured、--version、--hash 和 --checkapi 等独立输入决定。
2. Soong 输入
2.1 AIDL library
Android 17 新增的 aidl_library 模块不是代码生成器,而是为依赖方提供 AIDL 源文件和 include 目录。strip_import_prefix 决定剩余目录如何映射为 package 路径,deps 决定 include 根目录 如何传递。
源码文件:build/soong/aidl_library/aidl_library.go
type aidlLibraryProperties struct {
Srcs []string `android:"path"`
Hdrs []string `android:"path"`
Strip_import_prefix *string
Deps []string `android:"arch_variant"`
}
type AidlLibraryInfo struct {
Srcs android.Paths
IncludeDirs depset.DepSet[android.Path]
Hdrs depset.DepSet[android.Path]
}
func (lib *AidlLibrary) GenerateAndroidBuildActions(ctx android.ModuleContext) {
includeDirs := depset.NewBuilder[android.Path](depset.PREORDER)
hdrs := depset.NewBuilder[android.Path](depset.PREORDER)
if len(lib.properties.Srcs) == 0 && len(lib.properties.Hdrs) == 0 {
ctx.ModuleErrorf("at least srcs or hdrs prop must be non-empty")
}
srcs := android.PathsForModuleSrc(ctx, lib.properties.Srcs)
files := android.PathsForModuleSrc(ctx, lib.properties.Hdrs)
if lib.properties.Strip_import_prefix != nil {
srcs = android.PathsWithModuleSrcSubDir(ctx, srcs,
android.String(lib.properties.Strip_import_prefix))
files = android.PathsWithModuleSrcSubDir(ctx, files,
android.String(lib.properties.Strip_import_prefix))
}
includeDir := android.PathForModuleSrc(ctx,
proptools.StringDefault(lib.properties.Strip_import_prefix, ""))
includeDirs.Direct(includeDir)
hdrs.Direct(files...)
for _, dep := range ctx.GetDirectDepsProxyWithTag(aidlLibraryTag) {
if info, ok := android.OtherModuleProvider(ctx, dep, AidlLibraryProvider); ok {
includeDirs.Transitive(info.IncludeDirs)
hdrs.Transitive(info.Hdrs)
}
}
android.SetProvider(ctx, AidlLibraryProvider, AidlLibraryInfo{
Srcs: srcs, IncludeDirs: includeDirs.Build(), Hdrs: hdrs.Build(),
})
}这里的关键状态是 AidlLibraryInfo provider。它被依赖方读取,不会自行执行 aidl;因此只修改 hdrs 不会生成语言绑定,只有 srcs 被下游生成规则作为输入时才会产生输出。Soong 单元测试 TestAidlLibrary 断言了 strip_import_prefix 和传递依赖得到的 include 目录, TestAidlLibraryWithNoSrcsHdrsDeps 断言空模块在分析阶段失败。
2.2 生成规则
稳定接口通常由 aidl_interface 生成多个带语言后缀的 source 模块。system/tools/aidl/build/ aidl_gen_rule.go 把每个输入文件映射为一个输出文件,并把所有源文件组成 phony 输入,防止 同一接口的依赖变更没有触发其他文件重新生成。
源码文件:system/tools/aidl/build/aidl_gen_rule.go
func (g *aidlGenRule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
srcs, nextImports := getPaths(ctx, g.properties.Srcs, g.properties.AidlRoot)
g.deps = getDeps(ctx, g.getImports(ctx))
allSrcsPhony := android.PathForPhony(ctx, fmt.Sprintf(
"%s_%s_%s_all_srcs", strings.ReplaceAll(ctx.ModuleDir(), "/", "_"),
ctx.ModuleName(), ctx.ModuleSubDir()))
ctx.Build(pctx, android.BuildParams{
Rule: blueprint.Phony, Output: allSrcsPhony, Inputs: srcs,
})
g.implicitInputs = append(g.implicitInputs, g.deps.implicits...)
g.implicitInputs = append(g.implicitInputs, g.deps.preprocessed...)
g.implicitInputs = append(g.implicitInputs, allSrcsPhony)
g.importFlags = strings.Join(wrap("-I", g.deps.imports, ""), " ")
g.nextImportFlags = strings.Join(wrap("-N", nextImports, ""), " ")
for _, src := range srcs {
outFile, headers := g.generateBuildActionsForSingleAidl(ctx, src)
g.genOutputs = append(g.genOutputs, outFile)
g.genHeaderDeps = append(g.genHeaderDeps, headers...)
}
ctx.SetOutputFiles(g.genOutputs.Paths(), "")
ctx.SetOutputFiles(g.genHeaderDeps, "headers")
}-I 和 -N 不是同一个概念:-I 指向可解析 import 的目录,-N 表示 next imports,供 依赖文件和后续生成动作建立关系。action 的 implicit inputs 决定 Ninja 何时重跑;编译器只收到 已组装好的命令行,不知道 Soong 的 module graph。
3. 命令参数
system/tools/aidl/options.h 将命令行分成语言、任务、输入输出、稳定性和诊断配置。任务枚举明确 区分普通编译、预处理、dump API、check API 和 mappings,不应把所有任务都称为“生成代码”。
相关源码:
system/tools/aidl/options.hoptions.cpp
class Options final {
public:
enum class Language { UNSPECIFIED, JAVA, CPP, NDK, RUST, CPP_ANALYZER };
enum class Task { HELP, COMPILE, PREPROCESS, DUMP_API, CHECK_API, DUMP_MAPPINGS };
enum class CheckApiLevel { COMPATIBLE, EQUAL };
enum class Stability { UNSPECIFIED, VINTF };
Language TargetLanguage() const { return language_; }
Task GetTask() const { return task_; }
bool IsStructured() const { return structured_; }
Stability GetStability() const { return stability_; }
uint32_t GetMinSdkVersion() const { return min_sdk_version_; }
const std::set<std::string>& ImportDirs() const {
return as_previous_version_ ? previous_import_dirs_ : import_dirs_;
}
const std::vector<std::string>& InputFiles() const { return input_files_; }
const std::string& OutputFile() const { return output_file_; }
const std::string& OutputDir() const { return output_dir_; }
const std::string& PreviousApiDir() const { return previous_api_dir_; }
bool IsLatestUnfrozenVersion() const { return !PreviousApiDir().empty(); }
};Android 17 的默认最低 SDK 也由 Options 定义:Java 为 1,C++ 为 23,NDK 为 29,Rust 为 31。 Soong 的 min_sdk_version 会转成 --min_sdk_version;这会改变生成代码选择的 Parcel API, 不是只影响编译器警告。
4. 输入解析
4.1 加载阶段
aidl.cpp::load_and_validate_aidl 先解析主文件,再解析预处理文件和 import。import 名称不是 直接拼接当前文件目录,而是交给 ImportResolver 在每个 include 根目录中扫描 package/path/Type.aidl。找不到或找到多个候选都会产生诊断。
源码文件:system/tools/aidl/aidl.cpp
for (const auto& import : document->Imports()) {
if (typenames->IsIgnorableImport(import)) {
continue;
}
string import_path = import_resolver.FindImportFile(import);
if (import_path.empty()) {
err = AidlError::BAD_IMPORT;
continue;
}
import_paths.emplace_back(import_path);
auto imported_doc = Parser::Parse(import_path, io_delegate, *typenames);
if (imported_doc == nullptr) {
AIDL_ERROR(import_path) << "error while importing " << import;
err = AidlError::BAD_IMPORT;
}
}
if (err != AidlError::OK) return err;类型引用未在第一次扫描中解析时,resolver 会再次调用 FindImportFile,解析文件后重试 AidlTypeSpecifier::Resolve。因此“文件在磁盘上”不足以证明 import 可用:它必须位于某个 -I 根目录下,并且 canonical name 到路径的映射唯一。
4.2 类型解析
加载成功后,编译器运行 ResolveReferences、AidlTypenames::Autofill 和每个定义类型的 CheckValid。接口、structured parcelable、unstructured parcelable、enum 和 union 是不同的 AST 类型;后端选择发生在这些检查之后。
源码文件:system/tools/aidl/aidl_language.cpp
bool AidlTypeSpecifier::Resolve(const AidlTypenames& typenames,
const AidlScope* scope) {
AIDL_FATAL_IF(IsResolved(), this);
std::string name = unresolved_name_;
if (scope) name = scope->ResolveName(name);
AidlTypenames::ResolvedTypename result = typenames.ResolveTypename(name);
if (result.is_resolved) {
fully_qualified_name_ = result.canonical_name;
split_name_ = Split(fully_qualified_name_, ".");
defined_type_ = result.defined_type;
}
return result.is_resolved;
}List<T>、Map<K,V> 的参数限制也在 AidlTypeSpecifier::CheckValid 中执行。例如 Map 的 key 必须是 String,List 不接受任意 primitive 类型;这类错误发生在生成 Java/C++ 文件之前, 所以不能靠修改生成文件绕过。
5. 语义校验
5.1 文件与声明
一个文件通常只能有一个顶层非 unstructured parcelable 类型,且 package 与文件路径必须一致。 接口方法还要检查重复参数名、关键字、void 参数和 _aidl 保留前缀。
源码文件:system/tools/aidl/aidl_language.cpp
// system/tools/aidl/aidl_language.cpp :: AidlMethod::CheckValid
if (IsOneway() && GetType().GetName() != "void") {
AIDL_ERROR(this) << "oneway method '" << GetName()
<< "' cannot return a value";
return false;
}
for (const auto& arg : GetArguments()) {
if (!argument_names.insert(arg->GetName()).second) {
AIDL_ERROR(this) << "method '" << GetName()
<< "' has duplicate argument name '" << arg->GetName() << "'";
return false;
}
if (IsOneway() && arg->IsOut()) {
AIDL_ERROR(this) << "oneway method '" << GetName()
<< "' cannot have out parameters";
return false;
}
if (arg->GetType().GetName() == "void") {
AIDL_ERROR(arg->GetType()) << "'void' is an invalid type for the parameter";
return false;
}
}oneway 不是“把同步调用改成异步”的注释,而是会改变允许的返回值和参数方向。违反约束时 load_and_validate_aidl 返回 BAD_TYPE,后端根本不会运行。
5.2 稳定性
当 Soong 为稳定接口追加 --structured 时,unstructured parcelable 会被拒绝,除非它是被允许 的稳定类型。@VintfStability 还要求同时带有 --structured 与 --stability vintf;两者 任意缺失都会返回 NOT_STRUCTURED。
源码文件:system/tools/aidl/aidl.cpp
// system/tools/aidl/aidl.cpp :: load_and_validate_aidl
if (defined_type->IsVintfStability()) {
bool success = true;
if (options.GetStability() != Options::Stability::VINTF) {
AIDL_ERROR(defined_type)
<< "Must compile @VintfStability type w/ aidl_interface 'stability: \"vintf\"'";
success = false;
}
if (!options.IsStructured()) {
AIDL_ERROR(defined_type)
<< "Must compile @VintfStability type w/ aidl_interface --structured";
success = false;
}
if (!success) return AidlError::NOT_STRUCTURED;
}这解释了一个常见误判:同一份 .aidl 在 Java 不稳定后端可能可以编译,在 NDK/Rust structured 后端却失败。差异来自 Options 和 backend 的约束,不是 parser 随机行为。
6. 方法编号
解析并校验类型后,编译器遍历所有接口调用 CheckAndAssignMethodIDs。带有显式编号的方法保留 其编号,未指定的方法按稳定规则分配;版本/hash 元信息还可能注入 getInterfaceVersion 和 getInterfaceHash 方法。
源码文件:system/tools/aidl/aidl.cpp
if (!CheckAndAssignMethodIDs(interface->GetMethods())) {
err = AidlError::BAD_METHOD_ID;
}方法编号是生成代码和 Binder 事务分发之间的契约。修改方法顺序不能被描述成“只是源码重排”; 必须检查 API dump、显式 ID 和下游生成物是否仍保持兼容。--version 非空时,MetaMethodVisitor 会把版本查询方法加入 AST;--hash 非空时加入 hash 查询方法,后端再把它们一起生成。
7. 后端输出
compile_aidl 对每个输入文件的每个 defined type 选择一个 generator。Java 处理接口和结构化 类型;C++、NDK、Rust 走各自 generator;cpp-analyzer 只生成分析用途输出。输出文件名由 OutputFile 或 package 路径计算,依赖文件在 generator 之前写入。
源码文件:system/tools/aidl/aidl.cpp
相关函数/类型:compile_aidl
for (const auto& defined_type : document->DefinedTypes()) {
string output_file_name = options.OutputFile();
if (output_file_name.empty() && !options.OutputDir().empty()) {
output_file_name = GetOutputFilePath(options, *defined_type);
if (output_file_name.empty()) return false;
}
if (!write_dep_file(options, *defined_type, imported_files, io_delegate,
input_file, output_file_name)) {
return false;
}
bool success = false;
if (lang == Options::Language::CPP) {
success = cpp::GenerateCpp(output_file_name, options, typenames,
*defined_type, io_delegate);
} else if (lang == Options::Language::NDK) {
ndk::GenerateNdk(output_file_name, options, typenames,
*defined_type, io_delegate);
success = true;
} else if (lang == Options::Language::JAVA) {
java::GenerateJava(output_file_name, options, typenames,
*defined_type, io_delegate);
success = true;
} else if (lang == Options::Language::RUST) {
rust::GenerateRust(output_file_name, options, typenames,
*defined_type, io_delegate);
success = true;
}
if (!success) return false;
}C++ backend的 header 输出由 Soong 另行声明为 implicit outputs;Java/Rust 规则只声明主输出和 depfile。生成器返回 false 时,编译器以非零状态退出,Soong 不应把旧生成文件当作本次成功结果。
7.1 Java
Java generator 在 generate_java_binder.cpp 中建立 StubClass,它继承 android.os.Binder, 并实现接口。生成器的责任是把 AIDL AST 映射到代码结构;真实的 transact 调用、线程切换和 Parcel 回收由生成代码与 Binder runtime 共同完成,不是 aidl 在编译期执行的动作。
源码文件:system/tools/aidl/generate_java_binder.cpp
StubClass::StubClass(const AidlInterface* interfaceType, const Options& options)
: Class(), options_(options) {
this->comment = "/** Local-side IPC implementation stub class. */";
this->modifiers = PUBLIC | ABSTRACT | STATIC;
this->what = Class::CLASS;
this->type = interfaceType->GetCanonicalName() + ".Stub";
this->extends = "android.os.Binder";
this->interfaces.push_back(interfaceType->GetCanonicalName());
MakeConstructors(interfaceType);
MakeAsInterface(interfaceType);
// 后续构造 asBinder、onTransact 和方法分发结构
}7.2 C++ 与 NDK
Soong 对 C++/NDK 输出会同时准备 header 目录和 staging 目录。接口名以 I 开头时,生成规则 去掉首个 I 再形成 BpX、BnX 文件名;NDK backend 额外使用 aidl/ include 前缀。这个 命名逻辑在 aidl_gen_rule.go,而类型到 Parcel 读写方法的映射在 aidl_to_cpp.cpp 与 aidl_to_ndk.cpp。
7.3 Rust
Rust backend 的最低 SDK 默认值是 31;--mockall 只在 Rust 生成规则追加,不能写进普通 AIDL 声明后期待 Java backend 理解。Soong 的 aidlRustRule 与 Java 规则都输出 depfile,但 Rust 生成文件后续由 Rust module 消费。
8. 稳定 API
稳定接口的 API dump 不是生成代码的副产品,而是单独的 AIDL task。dump_api 遍历输入文件, 把定义类型按 package 路径写到输出目录;Soong 通过 .hash 和版本目录保存接口的历史状态。
源码文件:system/tools/aidl/aidl_dumpapi.cpp
static string GetApiDumpPathFor(const AidlDefinedType& defined_type,
const Options& options) {
string package_as_path = Join(Split(defined_type.GetPackage(), "."),
OS_PATH_SEPARATOR);
return options.OutputDir() + package_as_path + OS_PATH_SEPARATOR
+ defined_type.GetName() + ".aidl";
}
bool dump_api(const Options& options, const IoDelegate& io_delegate) {
for (const auto& file : options.InputFiles()) {
AidlTypenames typenames;
if (internals::load_and_validate_aidl(file, options, io_delegate,
&typenames, nullptr) != AidlError::OK) {
return false;
}
for (const auto& type : typenames.MainDocument().DefinedTypes()) {
auto writer = io_delegate.GetCodeWriter(
GetApiDumpPathFor(*type, options));
DumpVisitor visitor(*writer, /*inline_constants=*/false);
type->DispatchVisit(visitor);
}
}
return true;
}check_api 要求两个输入目录,并用 compatible 或 equal 级别比较。compatible 允许新增接口 成员等被规则认可的变化,但会拒绝删除旧类型、删除旧方法、修改 enum 值等;equal 还会拒绝 新增类型。它只比较属于给定目录的类型,不把 imported 类型误算为本接口的公共 API。
源码文件:system/tools/aidl/aidl_checkapi.cpp
bool check_api(const Options& options, const IoDelegate& io_delegate) {
AIDL_FATAL_IF(!options.IsStructured(), AIDL_LOCATION_HERE);
AIDL_FATAL_IF(options.InputFiles().size() != 2, AIDL_LOCATION_HERE);
auto old_tns = LoadApiDump(options, io_delegate, options.InputFiles().at(0));
auto new_tns = LoadApiDump(options, io_delegate, options.InputFiles().at(1));
if (!old_tns.ok() || !new_tns.ok()) return false;
const auto level = options.GetCheckApiLevel();
// 收集目录内类型,随后按 compatible/equal 规则逐类型比较
// ...
}版本大于 1 时,Soong 在 aidl_gen_rule.go 中寻找上一个版本的 aidl_api/<name>/<version-1> 和 .hash,并追加 --previous_api_dir、--previous_hash。找不到历史目录或 hash 是构建 配置失败,不是编译器“自动从 current 推断”。
9. 依赖与增量
编译器可以写 depfile;Soong 规则通过 --ninja -d file.d 要求 Ninja 格式。依赖文件记录主输入 和 import 文件,Soong 还把所有源文件 phony 与预处理结果放进 implicit inputs。两者共同决定 增量重编译:depfile 发现实际 import,phony 保证同一接口内源文件变化不会漏掉其他 action。
取消或中断时,Soong action 的输出可能只存在 staging 目录或 depfile。下一次构建会先运行 aidlDirPrepareRule 清理/创建生成目录,再重新生成;不要手工编辑 out/soong/.intermediates 中的 Java 或 header 文件,因为它们不是稳定输入。
10. 失败矩阵
| 阶段 | 证据位置 | 失败例子 | 结果 | 恢复方式 |
|---|---|---|---|---|
| Soong provider | aidl_library.go | srcs 与 hdrs 都为空 | module error | 补源文件或 header |
| import | ImportResolver::FindImportFile | 找不到或多个同名文件 | BAD_IMPORT | 修正 -I/依赖和 package 路径 |
| AST | load_and_validate_aidl | 文件名、类型或声明不合法 | BAD_PACKAGE/BAD_TYPE | 修改 AIDL 源,不改生成物 |
| 语义 | AidlMethod::CheckValid | oneway 返回值、out 参数 | BAD_TYPE | 调整方法签名 |
| 稳定性 | load_and_validate_aidl | VINTF 缺 structured/stability | NOT_STRUCTURED | 同步 Soong 属性和注解 |
| 版本 | check_api | 删除旧方法或改变 enum 值 | false | 增加新版本或保持兼容 |
| 输出 | compile_aidl | writer/后端失败 | 非零退出 | 清理 staging 后重跑 action |
失败路径的共同不变量是:后端不能消费未通过校验的 AST,旧生成文件也不能被当作当前成功输出。 源码、depfile 和历史 API 目录分别由不同 owner 管理,恢复时必须回到对应 owner 的输入。
11. 可执行验证
11.1 import 路径
选择一个真实 aidl_interface,从 Soong 的 -I 参数中找到它的 import 根目录,再用以下搜索 确认 canonical name 与物理路径一致:
rg -n 'aidl_interface|aidl_library|imports:' frameworks packages system --glob 'Android.bp'
rg -n 'FindImportFile|BAD_IMPORT|ResolveReferences' system/tools/aidl这能证明源码配置和 resolver 的路径关系,不能证明某个产品的所有 variant 都使用同一个 include 集合。
11.2 API 兼容
对一个已有 aidl_api/<interface>/<version> 的接口,先比较 current 与上一版本,再观察 check_api 的错误位置:删除方法、修改 enum 值和改变类型会在 aidl_checkapi.cpp 的比较函数 中报告。测试可使用 system/tools/aidl/build/tests_1 等固定 fixture,但它们证明的是 AIDL 规则,不证明服务端运行时行为。
Android 17 的 aidl_unittest.cpp 还覆盖了以下对应测试:
RejectsOutParametersInOnewayInterface与RejectsOutParametersInOnewayMethod:输入 oneway + out 参数,断言明确错误文本。RejectRecursiveParcelable:输入直接递归 parcelable,断言recursive parcelable。VintfRequiresStructured:输入@VintfStability但缺--structured,断言NOT_STRUCTURED。ParsesNdkOnlyStableParcelable:检查语言后端对稳定 parcelable 的不同约束。
这些测试覆盖 parser/validator 的输入、断言和失败边界;它们没有证明生成 Java 代码在设备上 完成一次 Binder 调用,也没有证明厂商自定义后端。
12. 源码导航
建议按以下顺序阅读,而不是从生成的 Stub 文件倒推:
build/soong/aidl_library/aidl_library.go:provider、strip prefix 和 transitive include。system/tools/aidl/build/aidl_gen_rule.go:Ninja rule、依赖、staging、语言输出和版本 flags。system/tools/aidl/options.h:任务、语言、SDK、稳定性和历史 API 参数。system/tools/aidl/aidl.cpp:加载、解析、校验、method ID 和后端 dispatch。system/tools/aidl/import_resolver.cpp:canonical import 到物理路径。system/tools/aidl/aidl_language.cpp:类型解析与方法/参数语义校验。system/tools/aidl/generate_java_binder.cpp、aidl_to_cpp.cpp、aidl_to_ndk.cpp、aidl_to_rust.cpp:后端差异。system/tools/aidl/aidl_dumpapi.cpp与aidl_checkapi.cpp:稳定 API 调用链。
读者可以用一个接口复述完整主线:哪个 Soong module 提供 .aidl,哪条 -I 解析 import,哪个 校验先拒绝错误 AST,哪个 flag 改变生成输出,以及 API 版本变化由哪个 task 证明。能沿这条链 定位一次失败,才真正理解了 AIDL 编译器,而不是只记住生成类名。
