AIDL 后端

AIDL 后端是生成桩代码的目标。在使用 AIDL 文件时,您始终是采用特定语言在特定的运行时环境中使用这些文件。因此,您应该根据具体情况使用不同的 AIDL 后端。

AIDL 的后端如下所示:

后端 语言 API Surface 构建系统
Java Java SDK/SystemApi(稳定*) 全部
NDK C++ libbinder_ndk(稳定*) aidl_interface
CPP C++ libbinder(不稳定) 全部
Rust Rust libbinder_rs(不稳定) aidl_interface
  • 这些 API Surface 是稳定的,但其中许多 API(例如用于服务管理的 API)已预留给内部平台使用,应用无法使用。如需详细了解如何在应用中使用 AIDL,请参阅开发者文档
  • Rust 后端是在 Android 12 中引入的;NDK 后端从 Android 10 开始一直可用。
  • Rust crate 是基于 libbinder_ndk 构建的。APEX 使用 binder crate 的方式与系统端使用其他可编译单元的方式相同。Rust 部分会捆绑到 APEX 中,并在其内提供。这具体取决于 system 分区上的 libbinder_ndk.so

构建系统

根据不同的后端,可以采用两种方式将 AIDL 编译成桩代码。如需详细了解构建系统,请参阅 Soong 模块参考文档

核心构建系统

在任何 cc_java_ Android.bp 模块(或等效的 Android.mk 模块)中,可以将 .aidl 文件指定为源文件。在这种情况下使用的是 AIDL 的 Java/CPP 后端(而不是 NDK 后端),并且使用相应 AIDL 文件的类会自动添加到相应模块中。您可以在这些模块中的 aidl: 组下指定选项(例如 local_include_dirs,用于告知构建系统该模块中 AIDL 文件的根路径)。请注意,Rust 后端仅可与 Rust 搭配使用。在未被指定为源文件的 AIDL 文件中,系统会以不同的方式处理 rust_ 模块。aidl_interface 模块会生成一个名为 <aidl_interface name>-rustrustlib,您可以链接它。如需了解详情,请参阅 Rust AIDL 示例

aidl_interface

此构建系统使用的类型必须是结构化类型。为了实现结构化,Parcelable 必须直接包含字段,而不是直接以目标语言定义的类型的声明。如需了解结构化 AIDL 如何与稳定的 AIDL 配合使用,请参阅结构化 AIDL 与稳定的 AIDL

类型

您可以将 aidl 编译器视为类型的参考实现。在创建接口时,请调用 aidl --lang=<backend> ... 来查看生成的接口文件。在使用 aidl_interface 模块时,您可以在 out/soong/.intermediates/<path to module>/ 中查看输出。

Java/AIDL 类型 C++ 类型 NDK 类型 Rust 类型
boolean bool bool bool
byte int8_t int8_t i8
char char16_t char16_t u16
int int32_t int32_t i32
long int64_t int64_t i64
float float float f32
double double double f64
String android::String16 std::string String
android.os.Parcelable android::Parcelable 不适用 不适用
IBinder android::IBinder ndk::SpAIBinder binder::SpIBinder
T[] std::vector<T> std::vector<T> In: &[T]
Out: Vec<T>
byte[] std::vector<uint8_t> std::vector<int8_t>1 In: &[u8]
Out: Vec<u8>
List<T> std::vector<T>2 std::vector<T>3 In: &[T]4
Out: Vec<T>
FileDescriptor android::base::unique_fd 不适用 binder::parcel::ParcelFileDescriptor
ParcelFileDescriptor android::os::ParcelFileDescriptor ndk::ScopedFileDescriptor binder::parcel::ParcelFileDescriptor
interface 类型 (T) android::sp<T> std::shared_ptr<T> binder::Strong
parcelable 类型 (T) T T T
union 类型 (T)5 T T T
T[N] 6 std::array<T, N> std::array<T, N> [T; N]

1. 在 Android 12 或更高版本中,出于兼容性原因,字节数组使用 uint8_t 而不是 int8_t。

2. C++ 后端支持 List<T>,其中 TStringIBinderParcelFileDescriptor 或 Parcelable 中的一种。在 Android 13 或更高版本中,T 可以是除数组以外的任何非基元类型(包括 interface 类型)。AOSP 建议您使用类似 T[] 的数组类型,因为这些数组类型适用于所有后端。

3. NDK 后端支持 List<T>,其中 TStringParcelFileDescriptor 或 Parcelable 中的一种。在 Android 13 或更高版本中,T 可以是除数组以外的任何非基元类型。

4. Rust 代码的类型传递方式有所不同,具体取决于类型是输入(参数)还是输出(返回值)。

5. Android 12 及更高版本支持 union 类型。

6. 在 Android 13 或更高版本中,支持固定大小的数组。固定大小的数组可以有多个维度(例如 int[3][4])。在 Java 后端,固定大小的数组以数组类型表示。

方向性 (in/out/inout)

指定函数的参数类型时,您可以将其指定为 inoutinout。这可控制为 IPC 调用传递哪些方向信息。in 是默认方向,表示数据从调用方传递给被调用方。out 表示数据从被调用方传递给调用方。inout 是这两种方向的组合。不过,Android 团队建议您避免使用参数说明符 inout。如果您将 inout 用于具有版本化接口和旧版被调用方,那么仅在调用方中出现的额外字段会重置其默认值。对于 Rust,常规 inout 类型会收到 &mut Vec<T>,列表 inout 类型会收到 &mut Vec<T>

UTF8/UTF16

借助 CPP 后端,您可以选择字符串是采用 UTF-8 编码形式还是 UTF-16 编码形式。在 AIDL 中将字符串声明为 @utf8InCpp String 可将其自动转换为 UTF-8 编码形式。NDK 后端和 Rust 后端始终使用 UTF-8 字符串。如需详细了解 utf8InCpp 注解,请参阅 以 AIDL 编写的注解

是否可为 null

您可以对在 Java 后端中可为 null 的类型使用 @nullable 进行注解,以将 null 值提供给 CPP 和 NDK 后端。在 Rust 后端中,这些 @nullable 类型作为 Option<T> 公开。默认情况下,原生服务器会拒绝 null 值。只有 interfaceIBinder 类型例外,对于 NDK 读取和 CPP/NDK 写入操作,这两个类型始终可以为 null。如需详细了解 nullable 注解,请参阅 以 AIDL 编写的注解

自定义 Parcelable

自定义 Parcelable 是在目标后端中手动实现的 Parcelable。仅当您尝试为无法更改的现有自定义 Parcelable 添加对其他语言的支持时,才应使用自定义 Parcelable。

为了声明自定义 Parcelable,以便让 AIDL 了解它,AIDL Parcelable 声明如下所示:

    package my.pack.age;
    parcelable Foo;

默认情况下,这会声明一个 Java Parcelable,其中 my.pack.age.Foo 是实现 Parcelable 接口的 Java 类。

如需用 AIDL 声明自定义 CPP 后端 Parcelable,请使用 cpp_header

    package my.pack.age;
    parcelable Foo cpp_header "my/pack/age/Foo.h";

my/pack/age/Foo.h 中的 C++ 实现如下所示:

    #include <binder/Parcelable.h>

    class MyCustomParcelable : public android::Parcelable {
    public:
        status_t writeToParcel(Parcel* parcel) const override;
        status_t readFromParcel(const Parcel* parcel) override;

        std::string toString() const;
        friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
        friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
    };

如需用 AIDL 声明自定义 NDK Parcelable,请使用 ndk_header

    package my.pack.age;
    parcelable Foo ndk_header "android/pack/age/Foo.h";

android/pack/age/Foo.h 中的 NDK 实现如下所示:

    #include <android/binder_parcel.h>

    class MyCustomParcelable {
    public:

        binder_status_t writeToParcel(AParcel* _Nonnull parcel) const;
        binder_status_t readFromParcel(const AParcel* _Nonnull parcel);

        std::string toString() const;

        friend bool operator==(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
        friend bool operator!=(const MyCustomParcelable& lhs, const MyCustomParcelable& rhs);
    };

然后,您就可以在 AIDL 文件中将此 parcelable 用作类型,但 AIDL 不会生成它。 为 CPP/NDK 后端自定义 Parcelable 提供 <== 运算符,以便在 union 中使用它们。

Rust 不支持自定义 parcelable。

默认值

结构化 Parcelable 可以为这些类型的基元、String 和数组声明各字段的默认值。

    parcelable Foo {
      int numField = 42;
      String stringField = "string value";
      char charValue = 'a';
      ...
    }

在 Java 后端中,当默认值缺失时,字段会针对基元类型和非基元类型分别初始化为零值和 null

在其他后端中,如果未定义默认值,系统会使用默认初始化值初始化字段。例如,在 C++ 后端中,String 字段初始化为空字符串,而 List<T> 字段初始化为空 vector<T>@nullable 字段初始化为 null 值字段。

错误处理

Android 操作系统提供内置的错误类型,供服务在报告错误时使用。这些类型由 binder 使用,可供任何实现 binder 接口的服务使用。它们的使用在 AIDL 定义中有详尽记录,并且不需要任何用户定义的状态或返回值类型。

输出参数存在错误

当 AIDL 函数报告错误时,该函数可能无法初始化或修改输出参数。具体来说,如果错误发生在取消打包期间,而不是发生在处理事务本身期间,则可以修改输出参数。一般来说,如果 AIDL 函数报告错误,所有 inoutout 参数以及返回值(在某些后端中充当 out 参数)都应该被视为处于不确定状态。

应使用哪些错误值

许多内置错误值可以在任何 AIDL 接口中使用,但有些会以特殊的方式处理。例如,EX_UNSUPPORTED_OPERATIONEX_ILLEGAL_ARGUMENT 可用于描述错误情况,EX_TRANSACTION_FAILED 却不行,因为底层基础架构会对其进行特殊处理。如需详细了解这些内置值,请查看后端专用定义。

如果 AIDL 接口需要使用内置错误类型未涵盖的其他错误值,则可使用特殊的服务特定内置错误;此类错误允许包含由用户定义的服务特定错误值。这些服务特定错误通常在 AIDL 接口中定义为由 const intint 支持的 enum,并且不由 binder 解析。

在 Java 中,错误会映射到异常,例如 android.os.RemoteException。对于服务特定异常,Java 会使用 android.os.ServiceSpecificException 以及用户定义的错误。

Android 中的原生代码不使用异常。CPP 后端使用 android::binder::Status。NDK 后端使用 ndk::ScopedAStatus。AIDL 生成的每个方法都会返回这两个方法之一,用来表示相应方法的状态。Rust 后端使用与 NDK 相同的异常代码值,但会先将其转换为原生 Rust 错误(StatusCodeExceptionCode),然后再将其提供给用户。对于服务特定错误,返回的 StatusScopedAStatus 会使用 EX_SERVICE_SPECIFIC 以及用户定义的错误。

内置错误类型可在以下文件中找到:

后端 定义
Java android/os/Parcel.java
CPP binder/Status.h
NDK android/binder_status.h
Rust android/binder_status.h

使用各种后端

以下说明专用于 Android 平台代码。这些示例使用的是已定义的类型 my.package.IFoo。如需了解如何使用 Rust 后端,请参阅 Android Rust 模式页面上的 Rust AIDL 示例

导入类型

无论定义的类型是 interface、parcelable 还是联合类型,您都可以在 Java 中将其导入:

import my.package.IFoo;

或者,在 CPP 后端中导入:

#include <my/package/IFoo.h>

或者,在 NDK 后端中导入(请注意包含额外的 aidl 命名空间):

#include <aidl/my/package/IFoo.h>

或者,在 Rust 后端导入:

use my_package::aidl::my::package::IFoo;

虽然您可以使用 Java 导入嵌套类型,但在 CPP/NDK 后端,您必须包含其根类型的头文件。例如,在导入 my/package/IFoo.aidl 中定义的嵌套类型 BarIFoo 是文件的根类型)时,您必须为 CPP 后端添加 <my/package/IFoo.h>(或为 NDK 后端添加 <aidl/my/package/IFoo.h>)。

实现服务

若要实现服务,必须从原生桩类继承。该类从 Binder 驱动程序读取命令并执行您实现的方法。假设您的 AIDL 文件如下所示:

    package my.package;
    interface IFoo {
        int doFoo();
    }

在 Java 中,您必须从该类进行扩展:

    import my.package.IFoo;
    public class MyFoo extends IFoo.Stub {
        @Override
        int doFoo() { ... }
    }

在 CPP 后端中:

    #include <my/package/BnFoo.h>
    class MyFoo : public my::package::BnFoo {
        android::binder::Status doFoo(int32_t* out) override;
    }

在 NDK 后端(请注意额外的 aidl 名称空间):

    #include <aidl/my/package/BnFoo.h>
    class MyFoo : public aidl::my::package::BnFoo {
        ndk::ScopedAStatus doFoo(int32_t* out) override;
    }

在 Rust 后端中:

    use aidl_interface_name::aidl::my::package::IFoo::{BnFoo, IFoo};
    use binder;

    /// This struct is defined to implement IRemoteService AIDL interface.
    pub struct MyFoo;

    impl Interface for MyFoo {}

    impl IFoo for MyFoo {
        fn doFoo(&self) -> binder::Result<()> {
           ...
           Ok(())
        }
    }

或者,使用异步 Rust:

    use aidl_interface_name::aidl::my::package::IFoo::{BnFoo, IFooAsyncServer};
    use binder;

    /// This struct is defined to implement IRemoteService AIDL interface.
    pub struct MyFoo;

    impl Interface for MyFoo {}

    #[async_trait]
    impl IFooAsyncServer for MyFoo {
        async fn doFoo(&self) -> binder::Result<()> {
           ...
           Ok(())
        }
    }

注册和获取服务

Android 平台中的服务通常通过 servicemanager 进程注册。除了下面的 API 之外,还有一些 API 会检查服务(这意味着,如果服务不可用,它们会立即返回)。请检查相应的 servicemanager 接口,了解具体详情。只有在针对 Android 平台进行编译时才能执行这些操作。

在 Java 中:

    import android.os.ServiceManager;
    // registering
    ServiceManager.addService("service-name", myService);
    // return if service is started now
    myService = IFoo.Stub.asInterface(ServiceManager.checkService("service-name"));
    // waiting until service comes up (new in Android 11)
    myService = IFoo.Stub.asInterface(ServiceManager.waitForService("service-name"));
    // waiting for declared (VINTF) service to come up (new in Android 11)
    myService = IFoo.Stub.asInterface(ServiceManager.waitForDeclaredService("service-name"));

在 CPP 后端中:

    #include <binder/IServiceManager.h>
    // registering
    defaultServiceManager()->addService(String16("service-name"), myService);
    // return if service is started now
    status_t err = checkService<IFoo>(String16("service-name"), &myService);
    // waiting until service comes up (new in Android 11)
    myService = waitForService<IFoo>(String16("service-name"));
    // waiting for declared (VINTF) service to come up (new in Android 11)
    myService = waitForDeclaredService<IFoo>(String16("service-name"));

在 NDK 后端(请注意额外的 aidl 名称空间):

    #include <android/binder_manager.h>
    // registering
    binder_exception_t err = AServiceManager_addService(myService->asBinder().get(), "service-name");
    // return if service is started now
    myService = IFoo::fromBinder(ndk::SpAIBinder(AServiceManager_checkService("service-name")));
    // is a service declared in the VINTF manifest
    // VINTF services have the type in the interface instance name.
    bool isDeclared = AServiceManager_isDeclared("android.hardware.light.ILights/default");
    // wait until a service is available (if isDeclared or you know it's available)
    myService = IFoo::fromBinder(ndk::SpAIBinder(AServiceManager_waitForService("service-name")));

在 Rust 后端中:

use myfoo::MyFoo;
use binder;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_binder(
        my_service,
        BinderFeatures::default(),
    );
    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");
    // Does not return - spawn or perform any work you mean to do before this call.
    binder::ProcessState::join_thread_pool()
}

在异步 Rust 后端中,使用单线程运行时:

use myfoo::MyFoo;
use binder;
use binder_tokio::TokioRuntime;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");

    // Sleeps forever, but does not join the binder threadpool.
    // Spawned tasks will run on this thread.
    std::future::pending().await
}

与其他选项的一个重要区别在于,使用异步 Rust 和单线程运行时时,我们不会调用 join_thread_pool。这是因为,您需要向 Tokio 提供一个线程,用于执行生成的任务。在此示例中,主线程将达到此目的。使用 tokio::spawn 生成的所有任务都将在主线程上执行。

在异步 Rust 后端中,使用多线程运行时:

use myfoo::MyFoo;
use binder;
use binder_tokio::TokioRuntime;
use aidl_interface_name::aidl::my::package::IFoo::BnFoo;

#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
    binder::ProcessState::start_thread_pool();
    // [...]
    let my_service = MyFoo;
    let my_service_binder = BnFoo::new_async_binder(
        my_service,
        TokioRuntime(Handle::current()),
        BinderFeatures::default(),
    );

    binder::add_service("myservice", my_service_binder).expect("Failed to register service?");

    // Sleep forever.
    tokio::task::block_in_place(|| {
        binder::ProcessState::join_thread_pool();
    });
}

使用多线程 Tokio 运行时,生成的任务不会在主线程上执行。因此,在主线程上调用 join_thread_pool 更有意义,这样主线程不只是处于空闲状态。您必须将调用封装在 block_in_place 中,才能退出异步上下文。

您可以请求接收通知,了解托管 binder 的服务何时终止。这有助于避免泄露回调代理,或协助恢复错误。对 binder 代理对象进行此类调用。

  • 在 Java 中使用 android.os.IBinder::linkToDeath
  • 在 CPP 后端中使用 android::IBinder::linkToDeath
  • 在 NDK 后端中使用 AIBinder_linkToDeath
  • 在 Rust 后端中,创建 DeathRecipient 对象,然后调用 my_binder.link_to_death(&mut my_death_recipient)。请注意,由于 DeathRecipient 拥有回调,因此只要您希望接收通知,就必须使该对象保持活跃状态。

来电者信息

收到内核 Binder 调用时,多个 API 中都会提供调用方信息。PID(即进程 ID)是指发送事务的进程的 Linux 进程 ID。UID(即用户 ID)是指 Linux 用户 ID。收到单向调用时,发起调用的 PID 为 0。在 binder 事务上下文之外时,这些函数会返回当前进程的 PID 和 UID。

在 Java 后端中:

    ... = Binder.getCallingPid();
    ... = Binder.getCallingUid();

在 CPP 后端中:

    ... = IPCThreadState::self()->getCallingPid();
    ... = IPCThreadState::self()->getCallingUid();

在 NDK 后端中:

    ... = AIBinder_getCallingPid();
    ... = AIBinder_getCallingUid();

在 Rust 后端,在实现接口时,指定以下内容(而不是采用默认值):

    ... = ThreadState::get_calling_pid();
    ... = ThreadState::get_calling_uid();

服务的 bug 报告和调试 API

错误报告在运行(例如,使用 adb bugreport 运行)时会从整个系统中收集信息,以帮助调试各种问题。 对于 AIDL 服务,bug 报告在通过服务管理器注册的所有服务中都使用二进制文件 dumpsys,以将其信息转储到 bug 报告中。您还可以在命令行中使用 dumpsys,通过 dumpsys SERVICE [ARGS] 从服务获取信息。在 C++ 和 Java 后端,您可以在 addService 中添加其他参数来控制服务转储顺序。在调试时,您还可以使用 dumpsys --pid SERVICE 获取服务的 PID。

如需在服务中添加自定义输出,您可以替换服务器对象中的 dump 方法,就像实现在 AIDL 文件中定义的任何其他 IPC 方法一样。执行这项操作时,您应限制应用权限 android.permission.DUMP 的转储或仅转储给特定 UID。

在 Java 后端中:

    @Override
    protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
        @Nullable String[] args) {...}

在 CPP 后端中:

    status_t dump(int, const android::android::Vector<android::String16>&) override;

在 NDK 后端中:

    binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;

在 Rust 后端,在实现接口时,指定以下内容(而不是采用默认值):

    fn dump(&self, mut file: &File, args: &[&CStr]) -> binder::Result<()>

动态获取接口描述符

接口描述符用于标识接口的类型。这在调试时或包含未知的 Binder 时非常有用。

在 Java 中,您可以使用如下代码获取接口描述符:

    service = /* get ahold of service object */
    ... = service.asBinder().getInterfaceDescriptor();

在 CPP 后端中:

    service = /* get ahold of service object */
    ... = IInterface::asBinder(service)->getInterfaceDescriptor();

NDK 后端和 Rust 后端不支持此功能。

静态获取接口描述符

有时(例如,在注册 @VintfStability 服务时),您需要知道静态获取接口描述符的情况。在 Java 中,您可以通过添加如下代码来获取描述符:

    import my.package.IFoo;
    ... IFoo.DESCRIPTOR

在 CPP 后端中:

    #include <my/package/BnFoo.h>
    ... my::package::BnFoo::descriptor

在 NDK 后端(请注意额外的 aidl 名称空间):

    #include <aidl/my/package/BnFoo.h>
    ... aidl::my::package::BnFoo::descriptor

在 Rust 后端中:

    aidl::my::package::BnFoo::get_descriptor()

枚举范围

在原生后端中,您可以遍历枚举可采用的所有可能值。出于代码大小方面的考虑,Java 目前不支持此操作。

对于使用 AIDL 定义的枚举 MyEnum,通过以下方式实现迭代。

在 CPP 后端中:

    ::android::enum_range<MyEnum>()

在 NDK 后端中:

   ::ndk::enum_range<MyEnum>()

在 Rust 后端中:

    MyEnum::enum_values()

线程管理

进程中的每个 libbinder 实例都维护着一个线程池。在大多数用例中,应该只有一个线程池,在所有后端之间共享。唯一的例外情况是,供应商代码可以另外加载一个 libbinder 副本,用于与 /dev/vndbinder 通信。由于此 Binder 位于单独的 Binder 节点上,因此相应线程池不共享。

对于 Java 后端,线程池只能增加大小(因为它已启动):

    BinderInternal.setMaxThreads(<new larger value>);

对于 CPP 后端,可以执行以下操作:

    // set max threadpool count (default is 15)
    status_t err = ProcessState::self()->setThreadPoolMaxThreadCount(numThreads);
    // create threadpool
    ProcessState::self()->startThreadPool();
    // add current thread to threadpool (adds thread to max thread count)
    IPCThreadState::self()->joinThreadPool();

同样,在 NDK 后端中可以执行以下操作:

    bool success = ABinderProcess_setThreadPoolMaxThreadCount(numThreads);
    ABinderProcess_startThreadPool();
    ABinderProcess_joinThreadPool();

在 Rust 后端中:

    binder::ProcessState::start_thread_pool();
    binder::add_service(“myservice”, my_service_binder).expect(“Failed to register service?”);
    binder::ProcessState::join_thread_pool();

使用异步 Rust 后端时,您需要两个线程池:Binder 和 Tokio。这意味着使用异步 Rust 的应用需要特别注意,尤其是在使用 join_thread_pool 时。如需了解详情,请参阅有关注册服务的部分

预留名称

C++、Java 和 Rust 预留了一些名称作为关键字或供语言专用。虽然 AIDL 不强制执行基于语言规则的限制,但使用与预留名称相同的字段名或类型名可能会导致 C++ 或 Java 编译失败。在 Rust 中,会使用“原始标识符”语法对字段或类型进行重命名,重命名后的名称可使用 r# 前缀进行访问。

建议您尽可能避免在 AIDL 定义中使用预留名称,以免造成不符合人体工程学的绑定或完全编译失败。

如果已经在 AIDL 定义中使用了预留名称,则可以安全地重命名字段,同时保持协议兼容;您可能需要更新代码才能继续构建,但任何已构建的程序都将继续互操作。

应避免使用的名称: * C++ 关键字 * Java 关键字 * Rust 关键字