文章目录
- 前言
- 1.引言
-
- [1.1 API设计的愿景](#1.1 API设计的愿景)
- [1.2 语法选择](#1.2 语法选择)
- 2.基础知识
-
- [2.1 proxy/skeleton架构](#2.1 proxy/skeleton架构)
- [2.2 通信方式](#2.2 通信方式)
-
- [2.2.1 event和triggers](#2.2.1 event和triggers)
- [2.2.2 methods](#2.2.2 methods)
- [2.2.3 fields](#2.2.3 fields)
- [2.3 服务是怎么连接的?](#2.3 服务是怎么连接的?)
-
- [2.3.1 Instance Identifiers and Instance Specifiers(II和IS)](#2.3.1 Instance Identifiers and Instance Specifiers(II和IS))
- [2.3.2 ResolveInstanceIDs()](#2.3.2 ResolveInstanceIDs())
- [2.3.3 开发者何时使用 InstanceIdentifier vs InstanceSpecifier?](#2.3.3 开发者何时使用 InstanceIdentifier vs InstanceSpecifier?)
- [2.4 客户端的API](#2.4 客户端的API)
-
- [2.4.1 proxy类的例子](#2.4.1 proxy类的例子)
-
- [2.4.1.1 HandleType 服务句柄](#2.4.1.1 HandleType 服务句柄)
- [2.4.1.2 构造函数](#2.4.1.2 构造函数)
- [2.4.1.3 findservice && startfindservice](#2.4.1.3 findservice && startfindservice)
- [2.4.1.4 自动更新机制](#2.4.1.4 自动更新机制)
- [2.4.2 event](#2.4.2 event)
-
- [2.4.2.1 event API](#2.4.2.1 event API)
- [2.4.3 Event数据访问机制](#2.4.3 Event数据访问机制)
-
- [2.4.3.1 SamplePtr](#2.4.3.1 SamplePtr)
- [2.4.3.2 事件数据访问机制解析](#2.4.3.2 事件数据访问机制解析)
- [2.4.3.3 轮询和事件驱动](#2.4.3.3 轮询和事件驱动)
- [2.4.4 methods](#2.4.4 methods)
-
- [2.4.4.1 异步调用与Future机制](#2.4.4.1 异步调用与Future机制)
- [2.4.4.2 fire and forget methods](#2.4.4.2 fire and forget methods)
- [2.4.4.3 方法结果的访问方式](#2.4.4.3 方法结果的访问方式)
- [2.4.4.4 取消methods的返回值](#2.4.4.4 取消methods的返回值)
- [2.4.5 fields](#2.4.5 fields)
- [2.4.6 triggers](#2.4.6 triggers)
- [2.5 服务端的API](#2.5 服务端的API)
-
- [2.5.1 服务端API](#2.5.1 服务端API)
-
- [2.5.1.1 method](#2.5.1.1 method)
- [2.5.1.2 fire and forget method](#2.5.1.2 fire and forget method)
- [2.5.1.3 错误返回](#2.5.1.3 错误返回)
- [2.5.1.4 events](#2.5.1.4 events)
- [2.5.1.5 field](#2.5.1.5 field)
- [2.5.1.6 trigger](#2.5.1.6 trigger)
- [3 ara::com与AUTOSAR元模型的关系:从设计到代码](#3 ara::com与AUTOSAR元模型的关系:从设计到代码)
前言
本文章内容主要参考AP AUTOSAR官方文档,主要内容为
1.COM组件有众多的api,对常见的api进行解析,并编写一些简单的demo(第二章)
2.COM的api和AUTOSAR adpative的关系是什么?开发流程是什么?(第三章)
这篇文章的写作思路是,通读经典的AP规范,然后在过程中加入注解,让AP规范听起来更简易。然后会加上些代码结构,让理解起来更简单
参考文档:
1.AUTOSAR_AP_EXP_ARAComAPI
2.AUTOSAR_AP_SWS_CommunicationManagement
1.引言
为什么AUTOSAR要发明另一个通信中间件API/技术,而市场上已经有数十种这样的技术(ROS,DDS...等)
原因:现有解决方案并不能全部满足AUTOSAR的要求!
AUTOSAR希望这个通信中间件可以满足以下的要求:
- 需要一个不绑定到具体网络通信协议的通信管理。它必须支持SOME/IP协议,但也必须具有更换协议的灵活性。
- AUTOSAR服务模型将服务定义为提供的方法、事件和字段的集合,这应该得到自然/直接的支持
- API应该同样良好地支持事件驱动模型和轮询模型来访问通信数据。后者通常是实时应用程序所需的,以避免不必要的上下文切换,而前者对于没有实时要求的应用程序来说更加方便。
- 能够无缝集成端到端保护,以满足ASIL要求。
- 支持静态(预配置)和动态(运行时)选择要通信的服务实例。
AUTOSAR的定义总是高层的抽象定义,它不会规定具体如何实现,它只会提供需求,就像一个产品经理一样。
上面这些要求说人话就是:
- 支持SOME/IP协议,并且可以无缝切换不同的通信协议,让上层应用开发者对此无感
- 需要支持SOA,服务的概念,并且支持method,event,field。简单来说就是说,这个通信中间件是SOA架构的。
- 需要支持事件驱动 和轮询两种方式来访问数据
- 需要有安全保护措施
- 需要可以动态部署不同的服务,像拼积木一样把服务能力搭建起来。服务就像积木,平台能提供什么能力取决于我们部署了什么服务,这个过程要像搭积木一样,我想拼哪个,想拆哪个,都是很轻松的完成的。
再高度总结一下,AUTOSAR的要求就是:
SOMEIP+SOA+无感切换通信协议+安全+动态部署
AUTOSAR新提出的通信中间件有很多概念是沿用了市面上用的比较多的中间件(ROS,DDS等),并且还融合了一些CP AUTOSAR的概念。举一些例子:
- proxy(stub)/skeleton的设计方法,这些是沿用了CORBA、Ice、CommonAPI、Java RMI的设计。这几个中间件可能大家不熟悉(我也没听过),下面是它们的介绍
CORBA(Common Object Request Broker Architecture)是一个由OMG组织制定的分布式对象标准,诞生于1990年代。它允许不同语言、不同平台的程序通过标准接口相互调用。CORBA使用IDL(接口定义语言)来定义服务接口,曾经在企业级应用中很流行,但现在已经相对过时,逐渐被更现代的技术替代。
Ice(Internet Communications Engine)是ZeroC公司开发的现代RPC框架,可以看作是CORBA的改进版。它提供了更好的性能、更简单的API,支持多种编程语言(C++、Java、Python等),并且特别注重网络效率和易用性。Ice在游戏、电信等领域有一定应用。
CommonAPI是汽车行业常用的进程间通信(IPC)框架,特别是在GENIVI联盟推动的车载信息娱乐系统中广泛使用。它提供了统一的API接口,底层可以对接不同的传输机制(如D-Bus、SOME/IP等),主要用于汽车电子系统中不同ECU或进程之间的通信。
Java RMI(Remote Method Invocation)是Java语言内置的远程方法调用机制,专门用于Java程序之间的分布式通信。它让你可以像调用本地对象一样调用远程Java对象的方法,使用起来相对简单,但仅限于Java生态系统内。
- 具有可配置接收端缓存的队列通信(JAVA RMI, DDS)
- 具有零拷贝能力的API,可以将内存管理转移到中间件
1.1 API设计的愿景
API设计的一个目标是使其尽可能精简 。这意味着,它应该只提供支持基于服务的通信范式所需的最小功能集,包括基本机制:方法、事件和字段。
AUTOSAR对"尽可能精简"这一概念的定义是:本质上,API应该只处理在服务消费者和服务提供者实现端处理方法、字段和事件通信的功能。
因此,ara::com不提供任何类型的组件模型或框架,这些模型或框架将负责诸如组件生命周期、程序流程管理或简单地根据相应应用程序的正式组件描述设置ara::com API对象等事务。
所有这些都可以轻松地在基本的ara::com API之上构建,并且不需要标准化来支持典型的协作模型。
简单来说,COM组件是一个精简的最小功能集,AUTOSAR鼓励使用者根据自己的需求,为COM组件进行封装,形成自己的中间件
1.2 语法选择
AUTOSAR决定真正支持AP的C++11/C++14,这与ara::com API设计非常契合。
为了增强可用性、舒适性和优雅性,ara::com API充分利用了C++特性,如智能指针、模板函数和类、经过验证的异步操作概念以及合理的运算符重载。
2.基础知识
2.1 proxy/skeleton架构
对于ara::com,AUTOSAR决定使用这种经典的代理/骨架架构模式,并相应地对其进行命名。

这张图描述了AP AUTOSAR的部分工作流程,先使用IDL定义服务接口(service interface),然后把对应的proxy和skeleton的代码生成出来,再基于这些proxy和skeleton代码进行application的开发。
在C++里面:
Skeleton类会生成一个纯虚类,开发人员需要继承这个纯虚类,然后实现服务端的逻辑。
Proxy类可以直接使用,但是一般来说开发人员都会去把这种生成的代码封装一层。
所以就是如图中所示的样子:蓝色的implementation是应用开发人员写的,橙色的proxy和skeleton是使用工具生成的代码。
2.2 通信方式
我们继续讨论如何在代理和骨架之间进行通信,这里会简单的介绍一下,后面会更详细的分析
ara::com定义了四种不同的机制来在服务器和客户端之间进行通信:
- 方法(Methods)
- 事件(Events)
- 字段(Fields)
- 触发器(Triggers)
在使用这些机制中的任何一个之前,必须先实例化服务,并且服务器必须向系统提供自己(OfferService())。然后,客户端需要使用代理查找并连接到服务实例(FindService()或StartFindService())。
2.2.1 event和triggers
event和triggers是一样的,所以放在一起讲。
当客户端应用程序连接到服务器后,它可以订阅(Subscribe())服务器提供的服务中的事件,如图4.2所示。

当事件有可用数据时,服务器应用程序将事件数据发送到通信管理中间件,中间件会通知所有订阅的客户端应用程序。然后,订阅者可以使用GetNewSamples()获取事件样本,可以直接获取,也可以通过由通知触发的回调函数(通过SetReceiveHandler()定义)获取。
触发器(Triggers)由服务器用于在发生特定条件时进行通知。它不传输任何数据。它使用与事件相同的订阅和通知机制。
总结:
- proxy可以通过subscribe订阅事件,skeleton可以通过Send()来发送事件
- proxy可以主动的去缓冲区拿事件,使用GetNewSamples(),也可以使用事件驱动机制,先注册一个SetReceiveHandler(),事件到达之后自动调用这个处理的回调函数。
- triggers是一个没有数据的event,用于通知
2.2.2 methods
通过基于方法的通信,客户端应用程序调用在远程服务器上执行的方法。这如图4.3所示。

Method 可能向客户端返回值,也可能不返回值。如果提供了返回值,则使用ara::core::Future 和ara::core::Promise模式来提供通信的非阻塞行为的可能性。
服务器可以配置为不同的方法调用处理模式。选项包括:
- 事件驱动,并发(kEvent):传入的服务方法调用以基于事件的方式处理。
- 事件驱动,顺序(kEventSingleThread):与kEvent相同,但基于单线程。
- 轮询(kPoll):传入的服务方法调用需要通过调用ProcessNextMethodCall以轮询方式显式处理。
2.2.3 fields
fields在功能上可以看成是events和methods的结合。field可以让客户端从服务端里获取(get)和修改(set)值,当值被修改之后,field也会被服务端发送给客户端。
- 客户端可以使用SetReceiveHandler()来注册回调函数,当服务端发送事件到达客户端的时候,回调函数会被调用。
- 客户端可以通过Set()方法去更新服务端的field字段,也可以通过get()方法去获取服务端的field字段
- 服务端可以使用RegisterSetHandler()来注册回调函数,当客户端使用set()方法更新field字段的时候会调用回调函数。
- 服务端可以调用update()函数去把field的值的变化通知给client
2.3 服务是怎么连接的?
2.3.1 Instance Identifiers and Instance Specifiers(II和IS)
这里使用英文来描述它们。因为平常也是使用缩写来描述这两个东西。
Instance Identifiers是一个非常核心的概念。在ara::com中,II在客户端用于搜索服务的特定实例,也就是说客户端是用II来在网络中查找服务实例的。服务端使用II来创建服务实例。
说了那么多,II的样子是什么样的?不同通信协议的II长的不一样.按照autosar的说法,比如someip协议就使用正整数,dds就使用字符串。但是都要满足一个要求就是,II可以被序列化。
开发者不需要知道II的值,II对开发者没有意义,开发者会使用的只有IS。
someip协议:
someip://1
someip://2
IPC:
ipc://1
ipc://2
DDS:
dds://service_1
dds://service_2
IS的样子长下面这样,其实就是一个字符串
/<context 0>/<context 1>/.../<context N>/<port name>
为什么需要这样呢?
- 为了方便开发,已经屏蔽下层具体的通信协议。
- 可以支持多绑定,一个IS可以对应多个II,不同的II的服务实例是可以使用不同的通信协议的
IS怎么使用呢?下面是伪代码:
服务端:
cpp
ara::core::InstanceSpecifier IS{
"/ServerApp/RootComponent/RadarService/PPort"
};
// 2. 创建服务(内部会自动解析)
RadarServiceSkeleton service(IS);
service.OfferService();
客户端:
cpp
// 1. 使用逻辑名称查找
ara::core::InstanceSpecifier spec{
"/ClientApp/RootComponent/RadarService/RPort"
};
// 2. 查找服务
auto handles = RadarServiceProxy::FindService(spec);
// 3. 创建代理
if (!handles.Value().empty()) {
RadarServiceProxy proxy(handles.Value()[0]);
}
2.3.2 ResolveInstanceIDs()
有一个关键的函数,叫ResolveInstanceIDs,入参是IS,返回值是一个Result,里面的模板参数是一个vector<InstanceIdentifier>,这也是对应了之前说的,一个IS可以对应很多个II。
cpp
ara::core::Result<ara::com::InstanceIdentifierContainer> ResolveInstanceIDs
(ara::core::InstanceSpecifier modelName);
它可能返回三种情况:
- 第一种是空返回,意味着IS没有对应的II,应该是配置文件没有配置对
- 第二种是返回一个列表(也就是vector了),里面只有一个元素。代表着这个IS只有一个binding,比如只有一个someip,或者IPC等
- 第三种是返回一个列表,列表里面有很多元素,这代表着可能是multi-binding,里面有多个通信协议,也可能是一个通信协议的binding,但是存在多个实例,比如IPC://1, IPC://2 ...
2.3.3 开发者何时使用 InstanceIdentifier vs InstanceSpecifier?
大多数情况下,开发者不需要手动调用 ResolveInstanceIDs() 进行转换。虽然ara::com对proxy和skeleton提供了两种构造函数,一种是传入IS的构造函数,一种是传入II的构造函数。
cpp
// ara::com 提供两种重载版本
// 版本1:使用 InstanceSpecifier(推荐)
Proxy(ara::core::InstanceSpecifier specifier);
// 版本2:使用 InstanceIdentifier(高级用法)
Proxy(ara::com::InstanceIdentifier identifier);
大多数情况直接使用版本1(IS)即可,对于版本2,文档中使用了exotic use case,罕见的时候,才会这么使用。我参考了一些供应商的实际实现,实际上II这个构造函数是没有实际价值的,它的作用和直接传递IS是一样的。
后续文档中接着补充了,劝诫开发者不要使用II这个构造函数。
2.4 客户端的API
标准列出了以下的客户端API,是比较常用的API
• FindService()
• StartFindService()
• StopFindService()
• Subscribe()
• Unsubscribe()
• GetSubscriptionState()
• SetSubscriptionStateChangeHandler()
• UnsetSubscriptionStateChangeHandler()
• GetNewSamples()
• GetFreeSampleCount()
• SetReceiveHandler()
• UnsetReceiveHandler()
• ResolveInstanceIDs()
• Field::Get()
• Field::Set()
下面会对所有的API都进行解释。在这个之前,会先讲一下proxy类的模板。
2.4.1 proxy类的例子
cpp
class RadarServiceProxy {
public:
/**
* \brief 实现是平台供应商特定的
*
* HandleType 必须包含创建代理所需的信息。
*
* 这些信息应该是隐藏的。
* 由于平台供应商负责创建句柄,
* 构造函数签名没有给出,因为用户不需要关心。
*/
class HandleType {
/**
* \brief 如果两个 ServiceHandle 代表相同的服务实例,
* 则认为它们相等。
*
* \param other 另一个句柄
*
* \return bool 是否相等
*/
inline bool operator==(const HandleType &other) const;
/**
* \brief 获取实例标识符
*
* \return 实例标识符的常量引用
*/
const ara::com::InstanceIdentifier &GetInstanceId() const;
};
/**
* StartFindService 不需要显式的版本参数,因为这个信息
* 在 ProxyClass 内部已经可用。
* 这意味着只会返回兼容的服务。
*
* \param handler 每当符合给定实例条件的服务可用性发生变化时,
* 都会调用此处理器。如果你使用这个版本的
* FindService,通信管理必须持续监控服务的
* 可用性,并在任何变化时调用处理器。
*
* \param instanceId 要搜索/查找的由 T 定义的服务类型的哪个实例。
*
* \return 此搜索/查找请求的句柄,应该用于停止可用性监控
* 以及相关的处理器触发。(参见 StopFindService())
*/
static ara::core::Result<ara::com::FindServiceHandle> StartFindService(
ara::com::FindServiceHandler<RadarServiceProxy::HandleType> handler,
ara::com::InstanceIdentifier instanceId);
/**
* 这是 StartFindService 方法的重载版本,使用实例规范,
* 通过服务实例清单进行解析。
*
* \param handler 服务可用性变化时的处理器
* \param instanceSpec 实例规范
*/
static ara::core::Result<ara::com::FindServiceHandle> StartFindService(
ara::com::FindServiceHandler<RadarServiceProxy::HandleType> handler,
ara::core::InstanceSpecifier instanceSpec);
/**
* 用于停止查找服务请求的方法(见上文)
*
* \param handle 要停止的查找句柄
*/
static void StopFindService(ara::com::FindServiceHandle handle);
/**
* 与 StartFindService(handler, instance) 相反,这个版本
* 是"一次性"查找请求,其特点是:
*
* - 同步的,即在查找完成并且匹配的服务实例结果列表
* 可用后返回。(如果当前不存在匹配的服务实例,列表可能为空)
*
* - 反映方法调用时的可用性。不会进行进一步的(后台)
* 可用性检查。
*
* \param instanceId 要搜索/查找的由 T 定义的服务类型的哪个实例。
*/
static ara::core::Result<ara::com::ServiceHandleContainer<RadarServiceProxy::HandleType>>
FindService(ara::com::InstanceIdentifier instanceId);
/**
* 这是 FindService 方法的重载版本,使用实例规范,
* 通过服务实例清单进行解析。
*
* \param instanceSpec 实例规范
*/
static ara::core::Result<ara::com::ServiceHandleContainer<RadarServiceProxy::HandleType>>
FindService(ara::core::InstanceSpecifier instanceSpec);
/**
* \brief 代理只能使用标识服务的特定句柄来创建。
*
* 此句柄可以是部署时定义的已知值,
* 也可以使用 ProxyClass::FindService 方法获得。
*
* \param handle 代理应该代表的服务的标识。
*/
explicit RadarServiceProxy(HandleType &handle);
/**
* 代理实例不可拷贝构造。
*/
RadarServiceProxy(RadarServiceProxy &other) = delete;
/**
* 代理实例不可拷贝赋值。
*/
RadarServiceProxy& operator=(const RadarServiceProxy &other) = delete;
/**
* \brief BrakeEvent 的公共成员
*/
events::BrakeEvent BrakeEvent;
/**
* \brief UpdateRate 的公共字段
*/
fields::UpdateRate UpdateRate;
/**
* \brief Calibrate 方法的公共成员
*/
methods::Calibrate Calibrate;
/**
* \brief Adjust 方法的公共成员
*/
methods::Adjust Adjust;
/**
* \brief LogCurrentState fire-and-forget 方法的公共成员
*/
methods::LogCurrentState LogCurrentState;
};
这个proxy类有几个重点
2.4.1.1 HandleType 服务句柄
cpp
class HandleType {
inline bool operator==(const HandleType &other) const;
const ara::com::InstanceIdentifier &GetInstanceId() const;
};
HandleType里面必须包含createInstance的所有内容,具体怎么实现是供应商实现的。这里提供一个思路,HandleType里面存放InstanceId和一个factory的指针,这个factory是一个纯虚类,下面会有多个factory子类,分别代表着不同的通信方式,比如someip_factory, IPC_factory,dds_factory等等。怎么创建一个服务实例呢?直接使用对应的通信方式的工厂类create即可。
总结来说,用户不需要知道HandleType是什么,只需要知道HandleType里面包含了能够找到远端服务的必要信息
(这只是一个思路,具体实现看不同的供应商,标准没有要求)

2.4.1.2 构造函数
cpp
/**
* \brief 代理只能使用标识服务的特定句柄来创建。
*
* 此句柄可以是部署时定义的已知值,
* 也可以使用 ProxyClass::FindService 方法获得。
*
* \param handle 代理应该代表的服务的标识。
*/
explicit RadarServiceProxy(HandleType &handle);
proxy对象只能使用handletype来构造。它是怎么被创建出来的呢?可以参考上面那个工厂类的设计,当然这个标准也并没有规定。
为什么要用Handle这个中间层?
文档给出了三个关键原因:
原因一:解耦设计
应用开发者 → FindService API → 获得Handle → 创建Proxy
- 开发者无法直接创建Handle(因为要保持与Communication Management独立)
- 只能通过ara::com提供的FindService API获取Handle
- 好处:确保只能创建与真实存在的服务实例对应的proxy
原因二:灵活性需求
// 同一个服务实例,可以创建多个Proxy实例
Handle handle = FindService(...);
Proxy proxy1(handle); // 第一个proxy实例
Proxy proxy2(handle); // 第二个proxy实例,共享同一个服务
- proxy实例包含状态信息(如事件缓存、注册的处理器等)
- 有些场景需要多个Proxy实例连接到同一个服务实例
- 如果FindService直接返回Proxy,ara::com无法知道你是想要:共享同一个proxy实例?还是 每次都创建新的proxy实例?
通过Handle中间层,决定权交给了开发者
原因三:避免意外拷贝
Proxy对象不允许拷贝,因为每一个proxy都很消耗资源。允许拷贝可能会导致没有必要的性能消耗。所以强制用户使用handletype来构建
实际使用的时候,应该这么使用
cpp
// 1. 查找服务,获取Handle
auto handles = FindService(...);
// 2. 从Handle创建Proxy(开发者明确的决定)
Proxy proxy1(handles[0]);
// 3. 如果需要另一个实例,再次显式创建
Proxy proxy2(handles[0]); // 连接到同一个服务
// 4. 不能拷贝
// Proxy proxy3 = proxy1; // 编译错误!
2.4.1.3 findservice && startfindservice
FindService是一次性查找,找不到就返回一个空列表。 StartFindService是持续监控,一旦服务可用性发生变化,那么就会调用用户传进来的回调函数。
可用性的意思是:不管服务是上线了,还是下线了,这个回调函数都会被执行。
findservice()用法
// 同步调用,返回当前时刻可用的服务实例
auto handles = Proxy::FindService(instanceId);
// handles可能为空(如果没有可用服务)
if (!handles.empty()) {
Proxy proxy(handles[0]);
// 使用proxy...
}
---------------------------------------------------------------------------------------------------------------
startFindService()用法
// 用户提供的回调函数
auto handler = [](ServiceHandleContainer<Proxy::HandleType> handles,
FindServiceHandle findHandle) {
std::cout << "服务可用性变化!当前有 " << handles.size() << " 个实例" << std::endl;
// 可以在这里停止监控
// StopFindService(findHandle);
};
// 启动持续监控
FindServiceHandle findHandle = Proxy::StartFindService(handler, instanceId);
// 稍后停止监控
Proxy::StopFindService(findHandle);
2.4.1.4 自动更新机制
当你从Handle创建了Proxy实例后,如果服务实例下线后又重新上线,这个已存在的Proxy实例还能继续使用吗?
答案是:能! 这就是自动更新机制。好处很明显,proxy对象创建出来就可以放在这里了,不需要关注它是否可用的问题。

2.4.2 event
Event会作为一个proxy的成员。注意,event是一个类!包括后面的method也是一个类
class RadarServiceProxy {
public:
// 每个事件都有一个对应的成员
events::BrakeEvent BrakeEvent; // 事件成员
events::ObjectDetected ObjectDetected; // 另一个事件
// 方法成员...
};
Event的API如下:
ara::core::Result<void> Subscribe(size_t maxSampleCount);
ara::com::SubscriptionState GetSubscriptionState() const;
void Unsubscribe();
size_t GetFreeSampleCount() const noexcept;
ara::core::Result<void> SetReceiveHandler(ara::com::EventReceiveHandler handler);
ara::core::Result<void> UnsetReceiveHandler();
ara::core::Result<void> SetSubscriptionStateChangeHandler(ara::com::SubscriptionStateChangeHandler handler);
void UnsetSubscriptionStateChangeHandler();
template <typename F>
ara::core::Result<size_t> GetNewSamples(F&& f, size_t maxNumberOfSamples = std::numeric_limits<size_t>::max());
2.4.2.1 event API
对每个函数进行简单的介绍
ara::core::Result<void> Subscribe(size_t maxSampleCount);
Subscribe函数可以订阅事件,函数立即返回,不等订阅完成。maxSampleCount指定最多可以缓存多少个事件样本,也就是说订阅后在本地会有一个缓冲区,maxSampleCount就是这个缓冲区大小。
ara::com::SubscriptionState GetSubscriptionState() const;
可以查阅当前的订阅状态,订阅状态有三种,成功订阅,未成功订阅,正在订阅中
bash
enum class SubscriptionState : uint8_t {
kSubscribed = 0,
kNotSubscribed,
kSubscriptionPending,
};
size_t GetFreeSampleCount() const noexcept;
GetFreeSampleCount()是获取订阅缓冲区中,还剩下多少个空闲的位置的。比如subscribe(10),然后来了三个事件,那调用GetFreeSampleCount,返回值就是7.
ara::core::Result<void> SetReceiveHandler(ara::com::EventReceiveHandler handler);
Event支持两种方式,一种是事件触发,一种是轮询。如果用户选择事件触发,那么就要使用
SetReceiveHandler函数来注册回调。当新的event到来的时候,注册的回调函数就会被执行。
新的event是如何定义的呢?调用了GetNewSample拿到的数据,就是老旧的,没拿过的就是新的。
ara::core::Result<void> SetSubscriptionStateChangeHandler(ara::com::SubscriptionStateChangeHandler handler);
subscribe状态一变化,就会触发回调函数
template <typename F>
ara::core::Result<size_t> GetNewSamples(F&& f, size_t maxNumberOfSamples = std::numeric_limits<size_t>::max());
可以从缓冲区里面拿出event数据,maxNumberOfSamples参数是一次最多能处理多少个数据。如果这个数字大于订阅时的数字就会报错。
注意:这个F,函数指针的签名必须是void(ara::com::SamplePtr<SampleType const>) ,这是标准规定的,引用下标准原文

subscribe规定了缓冲区多大,你只可能处理最多缓冲区大小的数据,不可能再多了
2.4.3 Event数据访问机制
这部分文档用了大篇章去描述它,应该是比较重要的一点。在讲Event数据访问机制之前要先补充一个点:SamplePtr
2.4.3.1 SamplePtr
SamplePtr是对unique_ptr的封装,在unique_ptr上加了一些功能,但是在内存管理的角度看,SamplePtr和unique_ptr是一样的。
unique_ptr核心特点是独占所有权。
- 一个 unique_ptr 独占它所指向的对象,这意味着同一时刻只能有一个 unique_ptr 指向某个对象。
- 当unique_ptr 被销毁时(比如离开作用域),它会自动释放所管理的对象,这样就避免了内存泄漏。
- 不可复制但可移动。你不能复制一个unique_ptr,但可以通过 std::move 转移所有权。
在subscribe的时候,会创建一个缓冲区,这个缓冲区实际上类型如下:
std::deque<std::unique_ptr<SampleType>> EventCache;
是一个队列,队列元素是unique_ptr.当然,返回给用户的时候,会把每一个unique_ptr拿出来,然后构造一个SamplePtr给用户使用
2.4.3.2 事件数据访问机制解析
从数据流动的角度看的流程是:
-
服务端发送事件到中间件缓冲区(Middleware Buffers)
-
用户调用GetNewSamples(),反序列化到样本槽(Sample Slot),然后调用用户调用getNewSamples()注册的回调函数 f(SamplePtr)
调用getNewSamples(), 中间件(COM)会做以下的操作:
- 检查当前持有的样本数,如果持有的样本超过订阅的样本,当用户调用getNewSamples(),就不允许用户再从中间件缓冲区拿数据到用户缓冲区了。
- 用户调用getNewSamples(),COM会一直把新来的事件数据从中间件缓冲区拿到用户缓冲区, 除了用户持有的样本数已经过多了,或者用户缓冲区已经满了
- 每个样本都会执行用户传进来的回调函数,getNewSamples()这个函数会返回这次执行了多少次回调函数。
这里需要说明一点的是,什么是持有的样本数?只要SamplePtr不被销毁,这个样本就被认为是用户持有的。SamplePtr是这个函数传入的lambda里的入参,也就是说,当函数执行完这个SamplePtr会直接被销毁掉。
文档里针对持有样本专门写了一个样例来说明:
// 1. 全局状态
std::unique_ptr<RadarServiceProxy> myRadarProxy;
std::deque<SamplePtr<const BrakeEvent::SampleType>> lastNActiveSamples;
// 2. 事件接收处理器
void handleBrakeEventReception() {
// 获取新样本,应用过滤和LastN策略
myRadarProxy->BrakeEvent.GetNewSamples(
[](SamplePtr<BrakeEvent::SampleType> samplePtr) {
if (samplePtr->active) { // 过滤条件
lastNActiveSamples.push_back(std::move(samplePtr));
if (lastNActiveSamples.size() > 10) {
lastNActiveSamples.pop_front();
}
}
// 不满足条件的样本自动释放
}
);
// 处理保留的样本
processLastBrakeEvents(lastNActiveSamples);
}
// 3. 主函数
int main() {
auto handles = RadarServiceProxy::FindService(instspec);
if (!handles.empty()) {
// 创建Proxy
myRadarProxy = std::make_unique<RadarServiceProxy>(handles[0]);
// 订阅事件(最多10个样本)
myRadarProxy->BrakeEvent.Subscribe(10);
// 注册接收处理器(事件驱动模式)
myRadarProxy->BrakeEvent.SetReceiveHandler(
handleBrakeEventReception
);
}
// 等待应用关闭...
}
使用了lastNActiveSamples,然后把samplePtr的所有权转移出去了
2.4.3.3 轮询和事件驱动
轮询的按周期去缓冲区拿事件数据,可以防止上下文一直抖动,下面是轮询的例子
// 示例:控制算法
void controlLoop() {
// 每10ms一个周期
Timer timer(std::chrono::milliseconds(10));
while (running) {
timer.wait(); // 等待下一个周期
// 获取最新数据
proxy.BrakeEvent.GetNewSamples(
[](auto sample) {
// 处理样本...
}
);
// 执行控制算法
computeControlOutput();
// 发送输出
sendControlCommand();
}
}
事件驱动的例子如下:
// 注册接收处理器
proxy.BrakeEvent.SetReceiveHandler(handleBrakeEventReception);
void handleBrakeEventReception() {
// 这个函数会被CM异步调用
// 当有新事件到达时自动触发
proxy.BrakeEvent.GetNewSamples(
[](auto sample) {
// 立即处理新数据
respondToEvent(*sample);
}
);
}
2.4.4 methods
和event一样,methods也是proxy里面的一个类。
文档给出的methods类样子
class Adjust {
public:
struct Output {
bool success;
Position effective_position;
};
ara::core::Future<Output> operator()(const Position &target_position);
};
核心组成:
- Output结构体:聚合所有OUT/INOUT参数和非void返回值
- operator():实际的方法调用操作符,包含所有IN/INOUT参数作为输入
Output结构体是一个聚合类型,将方法的所有输出封装在一起:
- 如果方法有非void返回值,它会作为结构体成员
- 所有OUT参数都作为结构体成员
- INOUT参数的输出部分也作为结构体成员
这种设计将复杂的多返回值场景统一为单一的结构体返回,简化了异步调用的处理。
ps: 如果只有一个返回值的methods,有一些供应商并不会把他放到结构体里面,这取决于供应商的实现。但是理论上按照标准文档是所有返回值都放入结构体里,和返回值个数无关。
2.4.4.1 异步调用与Future机制
methods的返回值是一个future,调用完后立刻返回future。这是ara::com的异步设计核心。Future的详细使用机制(如GetResult()、then()等)将在后续章节展开
用户只需要这么使用即可:
// 像调用本地函数一样
auto future = proxy.Adjust(target_position);
// 处理异步结果
future.then([](auto result) {
// 处理Output
});
方法包装类隐藏了序列化、网络传输、服务发现等复杂细节,开发者只需关注业务逻辑------传入参数,获取Future,处理结果。
2.4.4.2 fire and forget methods
methods分为两种,一种是客户端发送请求,服务端回复请求。一种是服务端不需要回复请求。不需要回复请求的被称为fire and forget方法,简称FF方法。
普通的方法类签名如下:
ara::core::Future<Output> operator()(const Position &target_position);
FF方法签名如下:
ara::core::Result<void> operator()();
FF方法的返回值是ara::core::Result<void>,不会有返回值,只可能会带有错误码(Result自带错误码)。
FF应用场景是什么呢?
单向方法适合对可靠性要求不高、但需要轻量通信的场景:
- 日志记录(如LogCurrentState)
- 统计信息上报
- 心跳信号
- 非关键通知
这些场景中,偶尔丢失一次调用不影响系统功能,但需要避免普通方法调用的往返开销和资源消耗。
2.4.4.3 方法结果的访问方式
我们怎么样拿到methods返回的结果呢?分为三种方式,阻塞等待,异步,轮询方式。
这三种方式都是通过Future这个数据结构来实现的,future的签名如下

它有几个重点高频使用函数
Result< T, E > GetResult () 阻塞直到结果可用,返回Result<T>不抛异常
T get () 阻塞直到结果可用,抛异常
bool is_ready () 非阻塞检查结果是否就绪
auto then (F &&func, ExecutorT &&executor) noexcept -> Future 注册回调函数,结果就绪时自动调用
void wait () 无限期阻塞等待
FutureStatus wait_for (const std::chrono::duration< Rep, Period > &timeoutDuration) 等待指定时长
下面会举例子把这些函数都用一遍。
阻塞等待方式获取数据,需要使用**get()或者GetResult()**函数
我个人习惯使用GetResult+HasValue+Value()这种组合来获取数据
// 调用方法
Future<Calibrate::Output> callFuture = service.Calibrate(myConfigString);
// 阻塞等待结果(异常方式)
Calibrate::Output output = callFuture.get(); // 错误时抛异常
// 或使用Result方式(不抛异常)
auto result = callFuture.GetResult();
if (result.HasValue()) {
Calibrate::Output output = result.Value();
}
这种模式下,方法调用虽然立即返回了Future,但通过get()或GetResult()的阻塞等待,从应用开发者角度看仍是同步调用。
异步调用方式获取数据
如果超时就抛弃,需要使用**wait_for()**函数
auto future = service.Adjust(position);
// 等待最多500ms
if (future.wait_for(std::chrono::milliseconds(500)) == future_status::ready) {
auto output = future.get(); // 不会阻塞
} else {
// 超时处理
}
如果返回值到了,触发回调函数。需要使用then()
ps: then()接受的callable(lambda)必须接收一个Future参数,这个例子也是使用then最简单的方法,清晰明了
service.Calibrate(config).then([](auto future) {
// 结果就绪时自动调用
auto result = future.GetResult();
if (result.HasValue()) {
processOutput(result.Value());
}
});
除了同步和异步,还可以使用轮询的方式,需要使用**is_ready()**函数
// 发起调用
auto future = service.Calibrate(config);
// 在控制循环中轮询
while (running) {
if (future.is_ready()) { // 非阻塞检查
auto output = future.get(); // 保证不阻塞
processOutput(output);
break;
}
// 继续其他周期性任务
doPeriodicWork();
}
2.4.4.4 取消methods的返回值
当不需要method的返回值的时候,应该提前主动取消它,而不是选择忽略它。即使不再关心结果,后台的通信机制仍在运行,仍会有性能上的消耗
怎么取消呢?显示调用future的构造函数即可。
Future<Calibrate::Output> calibrateFuture;
// 发起调用
calibrateFuture = service.Calibrate(config);
// 状态变化,结果不再需要
// 用默认构造的Future覆盖,触发原Future析构
calibrateFuture = Future<Calibrate::Output>();
// 或更简洁(C++11后)
calibrateFuture = {};
2.4.5 fields
field是method+event的合体版本。所以fields拥有event和method所有的api函数。
Event相关API(变化通知):
- Subscribe() / Unsubscribe()
- GetNewSamples()
- SetReceiveHandler() / UnsetReceiveHandler()
- GetSubscriptionState() / SetSubscriptionStateChangeHandler()
Method相关API(值操作):
- Get() - 返回Future,异步获取当前值
- Set(value) - 返回Future,异步设置并返回实际生效的值
给了一个例子来说明如何使用fields
// 订阅变化通知
proxy.UpdateRate.SetReceiveHandler([]() {
proxy.UpdateRate.GetNewSamples([](auto sample) {
std::cout << "UpdateRate changed to: " << *sample << std::endl;
});
});
proxy.UpdateRate.Subscribe(5);
// 主动获取当前值
proxy.UpdateRate.Get().then([](auto future) {
uint32_t currentRate = future.get();
std::cout << "Current rate: " << currentRate << std::endl;
});
// 设置新值
proxy.UpdateRate.Set(100).then([](auto future) {
uint32_t actualRate = future.get();
std::cout << "Rate set to: " << actualRate << std::endl;
});
2.4.6 triggers
Trigger是一种特殊的Event------无数据事件。它只传递"发生了"的信号,不携带任何数据负载。
由于无需缓存数据,Subscribe()不需要maxSampleCount参数:
// Trigger订阅
ara::core::Result<void> Subscribe();
// 对比Event订阅
ara::core::Result<void> Subscribe(size_t maxSampleCount);
triggers还提供了一个函数GetNewTriggers(), 返回自上次调用以来接收到的触发次数
// 返回自上次调用以来接收到的触发次数
size_t GetNewTriggers();
2.5 服务端的API
Skeleton类是ara::com服务端的核心,从服务接口描述(service interface)自动生成,与Proxy类对称:
- Proxy:服务消费端,调用远程服务
- Skeleton:服务提供端,实现服务功能
Skeleton是一个抽象类 ,为什么要这么设计呢?因为具体的业务逻辑要用户自己去实现
namespace skeleton {
class RadarServiceSkeleton {
public:
// 纯虚函数,必须由子类实现
virtual ara::core::Future<Calibrate::Output>
Calibrate(const ara::core::String& config) = 0;
virtual ara::core::Future<Adjust::Output>
Adjust(const Position& target_position) = 0;
// 其他服务方法...
};
}
C++语法规定,抽象类是不可以直接实例化的,因此用户必须要继承抽象类并实现服务方法
class MyRadarService : public RadarServiceSkeleton {
public:
// 实现纯虚函数
ara::core::Future<Calibrate::Output>
Calibrate(const ara::core::String& config) override {
// 实际的校准逻辑
Calibrate::Output output;
output.result = performCalibration(config);
// 返回Promise/Future
ara::core::Promise<Calibrate::Output> promise;
promise.set_value(output);
return promise.get_future();
}
ara::core::Future<Adjust::Output>
Adjust(const Position& target_position) override {
// 实际的调整逻辑
// ...
}
};
MyRadarService myService(instanceId);
myService.OfferService();
2.5.1 服务端API
Skeleton提供了一套完整的服务端API,主要包括:
- 服务生命周期:OfferService()、StopOfferService()
- 事件发送:Send()、Allocate()(事件相关)
- 方法处理:ProcessNextMethodCall()(轮询模式)
- Field处理:RegisterGetHandler()、RegisterSetHandler()、Field::Update()
下面文档里使用了一个demo去解释Skeleton类的各个成员函数,这里我把他们拆开来讲。
构造skeleton的函数,提供了三个。
// II形式构造函数
RadarServiceSkeleton(
ara::com::InstanceIdentifier instanceId,
ara::com::MethodCallProcessingMode mode = MethodCallProcessingMode::kEvent
);
static ara::core::Result<RadarServiceSkeleton> Create(
const ara::core::InstanceIdentifier& instanceID,
ara::com::MethodCallProcessingMode mode = MethodCallProcessingMode::kEvent
) noexcept;
-------------------------------------------------------------------------------
//IS形式构造函数
RadarServiceSkeleton(
ara::core::InstanceSpecifier instanceSpec,
ara::com::MethodCallProcessingMode mode = ...
);
所有构造函数的共同特点:
- 第二个参数mode指定方法调用处理模式,默认为kEvent(事件驱动)
- 与Proxy类似,Skeleton实例不可拷贝(禁用拷贝构造和拷贝赋值)
第二个参数MethodCallProcessingMode控制服务端如何处理客户端的方法调用请求
enum class MethodCallProcessingMode {
kEvent, // 事件驱动:COM自动调度方法调用
kPoll, // 轮询模式:应用主动调用ProcessNextMethodCall()
kEventSingleThread // 单线程事件驱动
};
如果选择事件驱动型,客户端发送method请求之后,服务端会自己处理请求,然后去执行对应的业务,然后返回一个值回去。
如果选择轮询,服务端需要自己调用ProcessNextMethodCall()来处理客户端的请求。
这里简单说一下为什么事件驱动都要提供两种事件驱动的方式。
从功能角度看,只需要kEvent------服务实现者总可以通过锁自行同步。但这里的考量是效率:
资源浪费场景:
假设服务实现需要大量同步(几乎所有方法都需要加锁)。如果使用kEvent,CM可能分配N个线程并发调用方法,但这些线程会立即在锁上阻塞,导致N-1个线程休眠------白白占用线程池资源。
// 创建轮询模式的Skeleton
MyRadarService service(instanceId, MethodCallProcessingMode::kPoll);
service.OfferService();
// 在主循环中处理方法调用
while (running) {
bool hasMore = service.ProcessNextMethodCall();
if (!hasMore) {
// 无待处理请求,可以做其他工作
}
}
ProcessNextMethodCall()从COM取出一个待处理的方法调用并执行,返回值指示是否还有更多待处理请求。这给予服务实现者完全的执行控制权,适合实时系统。
服务生命周期
ara::core::Result<void> OfferService();
void StopOfferService();
OfferService调用后,服务实例通过服务发现机制被客户端可见,可以接收方法调用和事件订阅。这个方法可以重复调用,不会有什么问题StopOfferService可以将服务实例从服务发现中移除,客户端无法再访问。也可以重复调用
用户拿到生成的skeleton类之后,当务之急是要继承父类,实现自己的业务逻辑,大概是如下:
class MyRadarService : public RadarServiceSkeleton {
public:
ara::core::Future<CalibrateOutput> Calibrate(
std::string configuration) override {
// 业务逻辑实现
CalibrateOutput output;
output.result = doCalibration(configuration);
ara::core::Promise<CalibrateOutput> promise;
promise.set_value(output);
return promise.get_future();
}
ara::core::Future<AdjustOutput> Adjust(
const Position& position) override {
// ...
}
// 实现单向方法(无返回值)
void LogCurrentState() override {
// 直接执行,无需返回Future
logToFile(currentState);
}
};
2.5.1.1 method
服务方法在Skeleton中是纯虚函数,必须由子类实现,举个例子,服务的method的签名如下:
struct AdjustOutput {
bool success;
Position effective_position;
};
virtual ara::core::Future<AdjustOutput> Adjust(
const Position& position) = 0;
- IN参数:直接映射为方法参数(非基本类型用const&传递)
- OUT参数/返回值:聚合在Output结构体中,通过Future返回
文档又一次强调了,为什么要返回future?因为业务处理可能是同步的也可能是异步 的,method不强制方法返回时必须完成处理。举一个异步处理的例子:
Future<AdjustOutput> Adjust(const Position& position) {
// 1. 创建Promise/Future对
ara::core::Promise<AdjustOutput> promise;
auto future = promise.get_future();
// 2. 启动新线程异步处理
std::thread t(
[this](const Position& pos, ara::core::Promise prom) {
prom.set_value(doAdjustInternal(pos));
},
std::cref(position), // const引用传递position
std::move(promise) // 移动Promise所有权到线程
).detach(); // 分离线程
// 3. 立即返回Future(可能未完成)
return future;
}
这样不会阻塞中间件,让效率提升
2.5.1.2 fire and forget method
与Proxy端的设计一致,FF方法的签名也是一样的,举个例子
virtual ara::core::Result<void> LogCurrentState() = 0;
为什么用Result呢?因为里面有可能有错误码。仅用于报告CM在调用方法时发生的本地可恢复网络绑定失败,例如网络层错误。
2.5.1.3 错误返回
服务端如果想返回错误给客户端,可以使用setError函数来设置错误,错误是在配置阶段就已经定义好了(在ARXML里面存放)
举个例子:
服务端遇到错误了,使用promise.SetError()去返回错误
Future<CalibrateOutput> Calibrate(const std::string& configuration) override {
ara::core::Promise<CalibrateOutput> promise;
auto future = promise.get_future();
// 检查配置参数
if (!checkConfigString(configuration)) {
// 检测到无效参数,设置应用错误
// SpecificErrorsErrc是在ARXML中定义的错误域
promise.SetError(SpecificErrorsErrc::InvalidConfigString);
} else {
// 正常处理
CalibrateOutput output = doCalibration(configuration);
promise.set_value(output);
}
return future;
}
客户端如何接受这个错误呢?再举个例子
先GetResult(),然后看一下hasValue()是否为true,如果不为true证明result里面有Error(),拿出来即可
auto future = proxy.Calibrate(config);
// 方式1:GetResult
auto result = future.GetResult();
if (!result.HasValue()) {
auto error = result.Error();
if (error == SpecificErrorsErrc::InvalidConfigString) {
// 处理无效配置错误
}
}
// 方式2:get(异常风格)
try {
auto output = future.get();
} catch (const ara::core::Exception& e) {
// 错误被转换为异常抛出
}
2.5.1.4 events
服务端的events类会包含更多的函数,最具代表的就是Send()
class BrakeEvent {
public:
using SampleType = RadarObjects;
// 方式1:传递已有数据
ara::core::Result<void> Send(const SampleType& data);
// 方式2:传递预分配数据
ara::core::Result<void> Send(SampleAllocateePtr<SampleType> data);
// 预分配内存
ara::core::Result<SampleAllocateePtr<SampleType>> Allocate();
};
服务端的event只需要考虑如何发送event即可。AUTOSAR规定了两种方式,两种方式有效率的区别。
第一种方式是直接发送,适合数据量很小或发送频率低的情况。调用返回后,event具体的数据可以被修改或销毁,因为在Send的时候已经把这个数据拷贝了一份。还是举个例子来说明:
RadarObjects eventData;
eventData.active = true;
// 填充数据...
myService.BrakeEvent.Send(eventData); // 内部会拷贝数据
第二种方式是预分配发送(Allocate + Send),这种用起来稍微麻烦一点,但是效率会高很多,用到的原理是零拷贝。
先对比一下两个发送方法的做法,直接发送是在用户程序里面创建数据,然后调用Send()函数,把用户的数据拷贝到COM中间件的空间里面,让COM去发送。这样就会存在一次拷贝。
使用Allocate的含义是直接在COM中间件的空间创建一块空间,然后直接在那块空间里面写数据,写完之后把这块空间的所有权从用户转移给中间件。这样就能少一次拷贝了
举个代码的例子:
auto samplePtr = myService.BrakeEvent.Allocate().Value();
// 填充数据
samplePtr->active = true;
fillVector(samplePtr->objects);
// 发送(转移所有权)
myService.BrakeEvent.Send(std::move(samplePtr));
Allocate()的函数声明如下:
返回值是一个Result,里面是一个SampleAllocateePtr,它其实就是unique_ptr。
上面那个例子,Allocate().Value(),是因为要把这个指针从Result里面取出来
core::Result<SampleAllocateePtr<SampleType>> Allocate()
template <typename T>
using SampleAllocateePtr = std::unique_ptr<T>;
文档还提到了为什么要使用unique_ptr?它的解释是:因为这块在COM中间件的内存块只可以有一个人拥有,它不允许这个内存块的所有权被拷贝了(unique_ptr只有移动构造函数,没有拷贝构造函数)
2.5.1.5 field
服务端负责Field的三个核心职责:
- 更新并通知Field值变化
- 响应客户端的Get()调用
- 处理客户端的Set()调用
field包装类长这个样子:
class UpdateRate {
public:
using FieldType = uint32_t;
// 更新field值,和event的Send()是一样的
ara::core::Result<void> Update(const FieldType& data);
// 注册Get处理器(可选)
ara::core::Result<void> RegisterGetHandler(
std::function<ara::core::Future<FieldType>()> getHandler
);
// 注册Set处理器(如果支持Set则必须)
ara::core::Result<void> RegisterSetHandler(
std::function<ara::core::Future<FieldType>(const FieldType&)> setHandler
);
};
一个一个函数拆开说明。
update函数用法和event的Send()是一样的,举个例子:
// 更新UpdateRate字段
uint32_t newRate = 100;
myService.UpdateRate.Update(newRate);
update函数被调用之后会发生以下的事情:
- 中间件会拷贝数据(调用返回后可以修改或销毁原数据)
- 如果配置了"on-change-notification",会自动通知所有订阅者
- 数据被序列化存储,用于后续的Get请求。
如果Field配置了Getter功能,必须在OfferService()之前至少调用一次Update()设置初始值。
RegisterGetHandler可以用来注册回调函数,处理客户端的Get()请求。其实不注册也可以,不注册的话,服务端会自动把上一次update过的值传给客户端。文档中的原话是:RegisterGetHandler是可选的,且在大多数情况下不应该使用,原因是CM已经缓存了最后一次Update()的值,可以自动响应Get请求,无需应用层介入。AUTOSAR不希望field计算还要耗费算力
不过如果实在要用,也可以像下面一样使用
// 注册Get处理器
myService.UpdateRate.RegisterGetHandler([]() -> ara::core::Future<uint32_t> {
// 自定义逻辑:可能从传感器实时读取
uint32_t currentRate = readFromSensor();
ara::core::Promise<uint32_t> promise;
promise.set_value(currentRate);
return promise.get_future();
});
与GetHandler相反,SetHandler在Field支持Set时是强制的。原因是,服务端必须验证客户端设置的值是否合法。
RegisterSetHandler要传入一个lamda,这个lamda表达式参数是set的值,返回值是一个future<fieldtype>。
为什么需要返回一个值呢?文档给出的解释是,虽然客户端设置了一个值,但是实际生效的值可能会和这个值有偏差,所以要返回一个实际生效的值。这在ADAS等领域有常见的应用场景。
服务端每次set完成之后,COM中间件都会自动调用一次update,把值更新一下。用户不需要显示调用
在应用的时候,用户应该自行维护一个field值,因为COM中间件的field值,用户无法获取。因此用户可以参照下面这段代码来使用field
class RadarServiceImpl : public RadarServiceSkeleton {
public:
RadarServiceImpl() : currentRate_(50) {
// 设置初始值
UpdateRate.Update(currentRate_);
// handler中使用副本
UpdateRate.RegisterSetHandler([this](const uint32_t& newValue) {
ara::core::Promise<uint32_t> promise;
// 可以访问当前值
if (newValue > currentRate_ * 2) {
promise.SetError(ErrorCode::kChangeTooLarge);
} else {
currentRate_ = newValue; // 更新副本
promise.set_value(newValue);
}
return promise.get_future();
});
}
private:
uint32_t currentRate_; // 应用层副本
};
总结一下实际使用field的时候应该怎么做:
| 机制 | 建议 | 原因 |
|---|---|---|
| GetHandler | 不注册 | CM自动处理更高效 |
| SetHandler | 必须注册 | 验证客户端输入 |
| Update调用 | 在SetHandler外调用 | SetHandler自动Update |
| 初始值 | OfferService前必须设置 | 确保Field有效 |
| 值副本 | 应用层维护 | handle需要访问当前值时 |
2.5.1.6 trigger
Trigger在服务端的实现同样简化,因为没有数据负载。
Send方法的签名如下:
ara::core::Result<void> Send();
3 ara::com与AUTOSAR元模型的关系:从设计到代码
前言:为什么要理解元模型?
在前面的章节中,我们一直专注于ara::com的运行时API------如何使用Proxy、Skeleton、Event等。但你可能会好奇:这些Proxy和Skeleton类是从哪里来的?答案就是:从AUTOSAR元模型自动生成。
理解元模型就像理解建筑图纸------它定义了系统的结构,而代码是根据图纸施工的结果。
总结来说就是:先定义,定义服务是什么?然后去实现服务的细节,最后进行部署。定义---->实现----->部署
AUTOSAR Adaptive Platform的开发可以分为三个层次,理解这三个层次是关键:
第一层是设计层,也就是配置,生成arxml。arxml里包含了很多关键元素,和COM有关系的有:
-
服务接口(Service Interface):
- 定义服务"长什么样"
- 包含哪些方法、事件、字段
- 数据类型是什么
-
软件组件类型(SoftwareComponentType):
- 定义可复用的软件模块
- 通过端口(Port)与外界交互
-
端口(Port):
- P-Port(Provided Port):提供端口,表示"我提供这个服务"
- R-Port(Required Port):需求端口,表示"我需要这个服务"
第二层是生成代码,工具链根据元模型自动生成C++代码:
- R-Port → 生成 Proxy类(客户端)
- P-Port → 生成 Skeleton类(服务端)
第三层是实现,开发者基于生成的代码编写应用:
- 继承Skeleton类,实现服务逻辑
- 使用Proxy类,调用远程服务

文档中有一个图片,画的就是上面说的三个步骤。
-
先定义一个service interface,叫RaderService,里面会包含很多method,event等。
-
然后创建两个软件组件(swc),A swc是客户端,B swc是服务端,它们都引用了同一份service interface。
-
由于A swc是客户端,所以生成了客户端的代码。B swc是服务端,所以生成了服务端的代码。
-
然后用户根据生成的代码去创建实例,创建出来了proxy instance和skeleton instance。
总结一下:
| 概念 | 通俗理解 | 技术术语 |
|---|---|---|
| 元模型 | 系统设计蓝图 | ARXML定义 |
| 服务接口 | 服务的"合同" | Service Interface |
| 软件组件 | 可复用的模块 | SoftwareComponentType |
| P-Port | "我提供服务" | Provided Port → Skeleton |
| R-Port | "我需要服务" | Required Port → Proxy |
| 实例化 | 创建具体对象 | 运行时对象 |