Skip to content

AIDL编译器

追踪 AIDL 文件的依赖收集、解析校验、稳定 API 检查和多语言代码生成,解释 aidl_interface 如何决定输入、参数与输出边界。

基于android-17.0.0_r1
AndroidAIDLBinderSoong

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

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

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.h
  • options.cpp
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

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

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

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

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

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

cpp
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

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

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

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 provideraidl_library.gosrcs 与 hdrs 都为空module error补源文件或 header
importImportResolver::FindImportFile找不到或多个同名文件BAD_IMPORT修正 -I/依赖和 package 路径
ASTload_and_validate_aidl文件名、类型或声明不合法BAD_PACKAGE/BAD_TYPE修改 AIDL 源,不改生成物
语义AidlMethod::CheckValidoneway 返回值、out 参数BAD_TYPE调整方法签名
稳定性load_and_validate_aidlVINTF 缺 structured/stabilityNOT_STRUCTURED同步 Soong 属性和注解
版本check_api删除旧方法或改变 enum 值false增加新版本或保持兼容
输出compile_aidlwriter/后端失败非零退出清理 staging 后重跑 action

失败路径的共同不变量是:后端不能消费未通过校验的 AST,旧生成文件也不能被当作当前成功输出。 源码、depfile 和历史 API 目录分别由不同 owner 管理,恢复时必须回到对应 owner 的输入。

11. 可执行验证 ​

11.1 import 路径 ​

选择一个真实 aidl_interface,从 Soong 的 -I 参数中找到它的 import 根目录,再用以下搜索 确认 canonical name 与物理路径一致:

bash
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 文件倒推:

  1. build/soong/aidl_library/aidl_library.go:provider、strip prefix 和 transitive include。
  2. system/tools/aidl/build/aidl_gen_rule.go:Ninja rule、依赖、staging、语言输出和版本 flags。
  3. system/tools/aidl/options.h:任务、语言、SDK、稳定性和历史 API 参数。
  4. system/tools/aidl/aidl.cpp:加载、解析、校验、method ID 和后端 dispatch。
  5. system/tools/aidl/import_resolver.cpp:canonical import 到物理路径。
  6. system/tools/aidl/aidl_language.cpp:类型解析与方法/参数语义校验。
  7. system/tools/aidl/generate_java_binder.cpp、aidl_to_cpp.cpp、aidl_to_ndk.cpp、aidl_to_rust.cpp:后端差异。
  8. system/tools/aidl/aidl_dumpapi.cpp 与 aidl_checkapi.cpp:稳定 API 调用链。

读者可以用一个接口复述完整主线:哪个 Soong module 提供 .aidl,哪条 -I 解析 import,哪个 校验先拒绝错误 AST,哪个 flag 改变生成输出,以及 API 版本变化由哪个 task 证明。能沿这条链 定位一次失败,才真正理解了 AIDL 编译器,而不是只记住生成类名。