您现在的位置是:首页 >技术教程 >IBM Rational Software Architect RealTime Edition (RSARTE) C++ RT Services Library 文档翻译网站首页技术教程

IBM Rational Software Architect RealTime Edition (RSARTE) C++ RT Services Library 文档翻译

虾球xz 2024-09-15 00:01:03
简介IBM Rational Software Architect RealTime Edition (RSARTE) C++ RT Services Library 文档翻译

IBM Rational Software Architect RealTime Edition (RSARTE) C++ RT Services Library

参考:https://rsarte.hcldoc.com/help/index.jsp?topic=%2Fcom.ibm.xtools.rsarte.webdoc%2Fusers-guide%2Foverview.html

本文档介绍了C++ RT Services库,它是从Rational Software Architect RealTime Edition(RSARTE)生成的实时应用程序所使用的运行时库。本文档的读者应已阅读了《在RSARTE中建模实时应用程序》一文,该文涵盖了本文中更详细解释的许多概念。所有屏幕截图都是在Windows平台上捕获的。

RT Services库提供了RSARTE支持的UML实时概念的运行时实现。某些服务的实现需要目标环境提供的功能。目标环境是指“surround”实时应用程序的东西,例如操作系统和实时应用程序将运行的目标硬件。下图显示了由RSARTE生成的实时应用程序的功能分层。
下图显示了RSARTE生成的实时应用程序的功能分层。
在这里插入图片描述

RT服务库将应用程序代码与目标环境隔离开来,以便可以为多个目标环境构建相同的实时应用程序。除了提供这种平台独立性之外,RT服务库还提供应用程序可以在运行时使用的某些服务。提供以下服务类别:
通信
时间控制
动态结构
并发处理
基于消息的处理

Target Configurations

RT Services库应该将目标代码(至少是从UML模型生成的目标代码)与各种目标环境的差异隔离开来。由于目标环境存在大量变化,因此必须有大量版本的RT Services库。每个这样的RT Services库版本称为目标配置,并且已配置为与特定的目标环境配合使用。以下是构成目标配置的一些参数:
操作系统(名称及必要时版本)
操作系统线程配置(单线程或多线程)
处理器架构
目标编译器(名称和版本)
RT Services库的特定目标配置为每个参数提供固定值。这些值可以组合在一起形成一个字符串,以紧凑的方式唯一地标识目标配置。以下是此类字符串的示例:
在这里插入图片描述

名称的第一部分是目标基本名称,用于唯一标识操作系统名称、版本(如果重要)和线程配置。在上面的示例中,操作系统名称为Windows。版本未指定,这表示目标配置可以在多个Windows版本上工作。字母"T"表示操作系统是多线程的(对于单线程的操作系统,使用字母"S")。

名字的第二部分跟在点号之后,是libset名称,用于确定处理器架构和目标编译器。在示例中,处理器架构为x64(即库编译为64位),编译器为Microsoft Visual C++ 17.0版本。

目标配置字符串在RT服务库的C++实现中被广泛使用。例如,如果您查看/rsa_rt/C++/TargetRTS目录,您会看到这些字符串。您还可以在"目标配置"选项卡中的"目标RTS配置"下拉菜单中看到这些字符串。
在这里插入图片描述

此下拉菜单中显示的目标配置字符串是从指定的“目标服务库”目录中动态提取的。因此,您可以指定任何包含RT Services库目标配置的目录。例如,您可以指定RSARTE安装的C++ / TargetRTS目录,以使用与RSARTE产品一起提供的目标配置之一。您还可以指定目录,其中创建了自己的RT Services库的自定义版本,适用于不作为RSARTE安装的一部分提供的自定义目标配置。有关目标RTS向导的文档,请参阅了解如何使用您选择的平台和编译器构建自己的RT Services库自定义版本。

Services

在本章节中,我们将逐个介绍RT Services库提供的每个运行时服务。这些服务可由您在模型中包含的操作代码使用,例如转换效果中的代码或状态入口行为代码。您还可以从任何手写的代码中访问这些服务,这些代码包含在您的应用程序中。

每个运行时服务都有一个或几个C++头文件,您必须从需要使用服务的C++源文件中包含它们。您还必须确保在构建应用程序时链接RT Services库。如果源文件是从UML模型生成的,则所需的#include通常由C++代码生成器自动添加。但是,如果缺少任何#include(无论是来自RT Services库还是来自您的代码需要使用的其他库),您只需选择上下文capsule并使用代码视图或代码编辑器编辑“头部引言”属性即可:
在这里插入图片描述

Communication Service

通信服务提供了capsule实例之间使用消息进行通信的方式。支持两种通信方式:

异步通信(发送):

在这种情况下,发送方capsule实例在发送消息时不会被阻塞。一旦它将消息发送到端口,它就可以继续执行。这种通信类型最常用,因为它提供了高吞吐量的消息,发送和接收capsule实例之间的依赖最小。

同步通信(调用)

在这种情况下,发送方capsule实例被阻塞,直到发送的消息到达接收方capsule实例,并且该实例已经回复接收到的消息。仅当发送方无法继续执行直到接收到发出的事件的响应(通常是因为接收方必须计算一些发送方在继续之前所需的数据)时,才应使用同步通信。在某些情况下,发送方可能不需要从接收方获取任何数据,但仍希望暂停其执行,直到接收方处理了消息。对于这些情况,可以执行一次调用,在此情况下,回复会在接收方处理消息后自动进行。

使用同步通信时,有四个重要的规则需要记住:

  1. 除非发送方执行了一个隐式回复的调用(invoke),否则接收方必须调用 reply() 函数来回复发送方。这必须在接收方仍处于触发状态转换中完成(更准确地说,必须在接收方进入新状态之前完成)。你可以在回复消息中传递返回数据,该数据将被提供给发送方。

  2. 发送方(即调用者)有责任分配能够保存回复数据的 RTMessage 对象。发送方还负责在这些对象不再需要时删除它们。使用 RTMessage::isValid() 确认回复对象与接收方所作的有效回复相对应。

  3. 不允许在不同线程之间执行调用。如果需要跨线程进行同步通信,则必须通过两个异步事件(call/reply)和一个状态来实现,其中发送方可以等待接收方回复,或者使用更低级别的同步原语,如信号量。

  4. 不允许执行导致循环的调用。例如,如果capsule A 调用capsule B,而capsule B 又尝试调用capsule A,则调用将在运行时失败并显示错误消息。

通过调用端口上的函数来执行同步和异步通信。端口被转换为生成自capsule的 C++ 类中的成员变量。端口变量的类型是 RTProtocol 的子类。此子类从类型化端口的协议生成,并且为协议中定义的每个事件包含一个函数。如果事件是一个输入事件(in-event),函数将返回 RTInSignal;
如果事件是一个输出事件(out-event),函数将返回 RTOutSignal。在 RTOutSignal 类中可以找到用于异步和同步通信的 send() 和 invoke() 函数。如果要求接收方回复 invoke,应调用带有回复缓冲区参数的 invoke() 版本,否则回复将被隐式执行。
如果发送端口是复制的(即具有“多个”重复性),则 send() 和 invoke() 函数将向所有端口实例广播事件,为每个实例创建一个消息。在这种情况下,如果你想将事件发送到特定的端口实例,则可以使用 sendAt() 和 invokeAt() 函数,这些函数的参数为要使用的端口实例的索引。索引是以 0 为基础的。

send 函数具有一个可选参数“priority”,该参数指定消息发送的优先级等级。此参数的默认值为“General”,这是适用于大多数消息的标准优先级。请注意,invoke 函数没有优先级参数,因为在此种情况下,事件直接由发送方处理,而不通过接收方的事件队列。因此,可以说同步发送的事件具有比任何其他异步发送的事件更高的优先级。
以下是使用通信服务发送事件的几个示例:

mul.getIncrement(12).send();

通过“mul”端口发送事件“getIncrement”。事件具有一个整数参数,并且发送的消息为该参数赋值为12。

reqPort.abort().sendAt(2, High); 

通过“reqPort”端口的第2个实例发送事件“abort”。事件没有任何参数。该端口是被复制的,事件的消息将通过索引为2的端口实例发送。该消息的优先级为“高”。

RTMessage* replies = new RTMessage[aPort.size()]; 
aPort.ack().invoke(replies); // Hangs until all replies are available 
for (int i = 0; i < aPort.size(); i++ ) { 
if (replies[i].isValid()) { 
// code to handle valid reply 
bool returnVal = (bool) replies[i].getData() 
} 
else { 
// code to handle invalid reply 
} 
} 
delete[] replies; 

同步地向复制端口“aPort”中的所有端口实例发送事件“ack”(广播)。请注意,每个接收方都必须使用类似于以下代码的代码回复收到的消息。

bPort.nack(false).reply(); 

在这里,使用事件“nack”进行回复,该事件将通过端口“bPort”发送。布尔值false作为返回数据被传递,发送者可以像上面所示从RTMessage对象中访问该数据。

comPort.runTest().invoke();

同步地通过端口“comPort”发送事件“runTest”。发送者将被阻塞,直到接收者已处理事件,接收者将立即处理该事件,然后再处理其他事件在其事件队列中。发送者不需要接收者提供任何响应值,因此一旦接收者已经处理了该事件,答复就会被隐含地发出。

Message Delivery

RT Services库确保在发送者和接收者之间的同一连接上发送的消息以同样的顺序接收。即使接收者容器实例在与发送者容器实例不同的线程中运行,这一点仍然是正确的。然而,如果应用程序分布在发送者和接收者运行在不同进程的情况下,这个保证可能不再成立。

在某些情况下,RT Services库无法将发送的消息传递给接收者。其中一种情况是当消息发送到未绑定端口(有线或无线)时。另一种可能发生在分布式应用程序中的情况是,物理通信介质中出现了数据丢失,导致消息无法到达接收者。

当RT Services库能够检测到消息未能传递时,将发出运行时错误。发送和调用(并回复)事件的函数在这种情况下将返回0,您的代码可以在此测试。例如:

if (!thePort.start().send()) 
log.log("Failed to send 'start' event"); 

在通过编程注册的非有线端口发送事件时,始终测试这些错误返回值是一种良好的实践。
另一种可能发生的情况是,消息可以到达接收方,但在将消息分派给接收方时,没有找到可以通过消息触发的转换。如果发生这种情况,则在接收方capsule类上调用虚函数unhandledMessage()。在几乎所有情况下,未处理的消息都是意外的,在这些情况下,将调用另一个虚函数unexpectedMessage()。可以重写此函数以定义在这种情况下应该发生什么。RTActor中函数的默认实现(所有生成的capsule类都继承自该类)是将错误消息打印到stderr。

Message Representation

RT Services库使用RTMessage类的对象来代表消息。这个类包含了以下信息:

  • 消息携带的参数数据。这个数据是未经类型指定的(void*),可以使用getData()函数来访问。
  • 消息的事件名称,如协议定义。
  • 消息被发送时的优先级级别。

RT Services库在将消息发送到接收方capsule实例时创建了一个RTMessage对象,并将其放置在消息队列中。它会一直在消息队列中保持,直到成为队列中的第一条消息,然后才会被分派(即传递)给接收方capsule实例。通常情况下,您应该将RTMessage对象及其包含的所有内容视为只读。当所有触发转换时执行的代码完成并且控制返回到RT Services库时,该内存将被RT Services库删除。

默认情况下,RT Services库按值传递所有事件参数数据,这意味着在发送消息时将复制数据。这种方法避免了来自不同线程的公共数据访问问题,但如果数据量大和/或发送次数大量,则复制数据可能过于昂贵。请参见避免复制消息数据以了解如何避免复制消息数据的方法。

对于同步通信(“调用”),事件参数数据不会被复制。因为这种通信可能不会跨越不同的线程而发生。

RT Services库使用类型描述符来知道如何复制用户定义类型的对象。如果一个类型没有类型描述符,则不能被复制,因此不能通过消息按值发送。有关类型描述符的更多信息,请参见类型描述符。

在开发应用程序时,请不要假设RT服务库只会在发送事件时将RTMessage对象复制一次。有时,它将被多次复制。例如,当使用模型调试器时,RT服务库会创建携带RTMessage对象副本的通知。因此,任何作为事件参数发送的数据对象都必须具有类型描述符,其中复制函数(例如复制构造函数)能够多次复制数据对象。请注意,即使在消息对象被其接收方使用后,仍可能发生复制。因此,接收方不能以防止其以后被正确复制的方式更改它。

为转换生成的函数通过 rtData 参数直接访问消息参数数据。该参数的类型为指向参数数据类型的指针,因此可以直接使用,无需进行转换。以下是一个转换函数的示例,其中触发事件具有 double 类型的事件参数。

INLINE_METHODS void Adder_Actor::transition5_increment_computed( const 
double * rtdata, INC_REQ::Base * rtport ) 
{ 
//{{{USR 
double inc = *rtdata; 
result += inc; 
//}}}USR 
} 

如果一个转换可以触发多个事件,并具有不同类型的事件参数数据,则rtData将是一个未经类型说明的指针,您需要将其转换为期望的类型。如果您需要在转换函数之外访问消息参数数据,例如在从capsule操作生成的函数中,则可以使用 getMsg() 函数或capsule类(RTActor的实例)的 msg 成员变量来访问当前处理的消息的 RTMessage 对象。

Avoiding to Copy Message Data

在很多情况下,消息携带的数据足够小,因此复制数据不会导致性能问题。但是,如果需要发送较大的数据,复制数据可能是成本过高的。特别是当数据不仅仅只发送一次,而是多次发送时,这种情况尤其明显。

避免 RT Services 库复制消息数据的一种解决方案是将数据的指针作为事件参数传递。但是,您需要确保该数据(现在在发送方和接收方之间共享)受到充分的线程保护,以防止不同线程同时访问(例如使用信号量或互斥锁)。您还必须确保在不再需要该数据时将其删除。

如果数据可移动,更好的解决方案是移动数据而不是复制数据。为使数据可移动,其类型描述符必须定义一个移动函数。如果数据类型是一个 C++ 类(使用 C++11 编译器编译),通常的方法是确保它有一个移动构造函数(可以显式定义,也可以由编译器自动生成)。类型描述符的移动函数可以简单地调用移动构造函数。例如:

(void)new( target ) MyClass( std::move(*source) ); 

有关类型描述符的详细信息,请参见类型描述符。

在发送事件时,您可以根据是否使用了对数据的左值或右值引用来决定是否应复制或移动其数据:

myPort.myEvent(data).send(); //Send by copy (lvalue ref to data) 
myPort.myEvent(std::move(data)).send(); //Send by move (rvalue ref to data) 

如果您试图通过移动方式发送数据,但数据不可移动,则数据仍将被复制。还请注意,如果您在复制的端口上发送事件(即具有多重性大于 1 的端口),则该数据只能移动一次。在这种情况下,数据将被移动到最后一个端口实例发送,并且所有其他实例都会复制数据。

如果接收到事件的capsule需要将其数据存储以供以后使用,它还可以通过将接收到的数据移动到capsule属性中来避免数据复制,例如:

someAttr = std::move(*rtdata); // Avoid copying the message object

要使 rtData 成为可移动的,必须将其声明为非const。转换函数上的属性 “Const rtdata parameter” 控制着 rtData 是否应该声明为const。
在这里插入图片描述

请注意,在其他情况下也会复制消息数据,例如在调用事件时(即同步通信),或者在将capsule实例化到可选capsule部件时提供初始化数据。目前,在这些情况下无法避免复制消息数据。

Deferring and Recalling Messages

可以推迟处理接收到的消息,以便稍后处理。为此,调用接收到的消息的 RTMessage 对象上的 defer() 函数即可。将消息推迟会将其放入推迟队列中。使用 RTInSignal 类提供的函数来操作该队列。例如,它具有可用于召回先前推迟的消息的 recall() 函数。当消息被召回时,它会从推迟队列移回消息队列,以便稍后再次分派给接收方capsule实例。请注意,推迟的消息会一直留在推迟队列中,直到被召回。必须确保不要忘记推迟队列中的消息。以下是推迟和召回消息的一些示例:

msg->defer();

推迟当前消息(即最近分派给capsule实例的消息)。

thePort.theEvent().recall();

recall端口 “thePort” 上事件 “theEvent” 的第一个被推迟的消息。该消息将被移动到消息队列的末尾。

thePort.theEvent().recallAll(1);

recall端口 “thePort” 上事件 “theEvent” 的所有被推迟的消息。被召回的消息将被移动到消息队列的前面,这意味着它们将是下一个要分派到capsule实例的消息。

thePort.theEvent().purge(); 

删除端口 “thePort” 上事件 “theEvent” 的所有被推迟的消息。如果 “thePort” 被复制了,这个函数将在所有端口实例上操作。
如果您需要推迟被调用事件的回复消息,以便该回复可以在不同的转换中进行处理,那么您不能使用 defer() 函数,因为回复消息的 RTMessage 对象通常位于调用 invoke() 的同一个函数中。但您可以复制回复消息并将其再次发送到同一个capsule实例。通过这种方式,您可以推迟处理回复消息,以使该消息不必在接收器处理被调用事件后立即发生。例如:

RTMessage reply; 
myport.myevent().invoke(&reply); 
sendCopyToMe(&reply);

当然,sendCopyToMe() 函数可以用于任何类型的事件,并且在您无法在单个转换中完全处理事件时非常有用。

Non-wired Ports

当包含capsule实例被初始化时,作为有线的端口会由 RT Services 库自动连接。相同的道理也适用于那些 “Registration Kind” 属性被设置为 “Automatic” 或 “Automatic (Locked)” 的非有线端口。但是,如果该属性被设置为 “Application”,则必须以编程方式将这些端口注册为 SPP 或 SAP 端口。当端口被注册后,RT Services 库将自动建立连接。
用于建立连接的函数位于 RTProtocol 类中。以下是一个示例:

p1.registerSAP("aPort"); 

将端口“p1”注册为名称为“aPort”的SAP端口。

Logging Service

RT Services 库通过 Log 协议提供基础日志服务。要使用此服务,必须为希望打印日志消息的capsule添加一个端口。该端口的类型应设置为 Log 协议。
在 C++ 中,由 Log 协议定义的端口会变成类型为 Log::Base 的成员变量。该类提供了几个向 stderr 写入字符串和其他数据类型的函数。以下是一些示例:

log.log("Hello");

打印字符串“Hello”,然后将回车返回日志

log.show(x);

将变量 x 的内容打印到日志中,不带回车符。
Log::Base 类提供了多个重载版本的 log() 和 show() 函数,以处理所有基本的 C++ 类型。但是,如果您想要打印用户定义类型(如 UML 类)的对象,则需要将此用户定义类型的类型描述符作为第二个参数提供。指定为类型描述符的编码函数将被调用,以获取可以打印的对象的字符串编码。例如:

log.log(&myClass, &RTType_MyClass); 

有关类型描述符的更多信息,请参见类型描述符。
请注意,要刷新日志,您必须调用log.commit()。如果忘记刷新日志,则在底层流缓冲区满之前,您可能不会在控制台中看到消息。

Timing Service

计时服务是通过 Timing 协议提供的。由该协议类型化的端口充当定时器,定时器将在特定时间点(绝对或相对于现在)或固定间隔发送超时事件。
实现 Timing 协议的 RT Services 库的 C++ 类是 Timing::Base,它包含了各种设置定时器超时、取消等操作的函数。仅设置一次超时的定时器称为一次性定时器。它可以设置为在指定的时间段(相对时间)之后或在特定的时间点(绝对时间)超时。定时器还可以设置为定期间隔重复超时,称为周期性定时器。虽然可以通过使用重复重置的一次性定时器来实现这样的定时器,但周期性定时器更准确。这是因为处理超时和重置定时器总是需要一些时间。此外,您还需要考虑定时器舍入误差,它可能会减少或增加时间漂移。
时间值由 RTTimespec 类表示。该类还具有一个静态函数 getclock(),根据当前时间值填充 RTTimespec 对象。您可以使用常规的 C++ 比较运算符(如 ‘<’、‘>=’、‘==’ 等)比较不同的 RTTimespec 对象。您还可以使用 ‘+’ 和 ‘-’ 运算符执行简单的时间算术运算。
在设置定时器时,也可以使用 std::chrono 库中的类型指定时间值。这些值将自动转换为 RTTimespec 对象。
以下是使用 Timing 服务的示例(“timer” 是按 Timing 协议类型化的端口):

timer.informIn(RTTimespec(5, 0)); 

将一次性计时器设置为从现在起5秒后过期(相对时间)

timer.informIn(std::chrono::seconds(5));
// or shorter with C++ 14 and using namespace std::chrono_literals; 
timer.informIn(5s); 

与上面相同,但使用std::chrono库

RTTimespec now; 
RTTimespec::getclock(now); 
timer.informAt(now + RTTimespec(5, 0));

将一次性计时器设置为从现在起5秒后过期(绝对时间)。

std::chrono::system_clock::time_point t = std::chrono::system_clock::now() 
+ std::chrono::seconds(5); 
timer.informAt(t); 

与上面相同,但使用std::chrono库

timer.informEvery(RTTimespec(5, 500000000)); 

将定期计时器设置为每5.5秒过期一次。第一次超时将在5.5秒后发生(相对时间)

timer.informEvery(std::chrono::milliseconds(5500)); 
// or shorter with C++ 14 and using namespace std::chrono_literals; 
timer.informEvery(5500ms);

与上面相同,但使用std::chrono库
与其他事件一样,计时器到期时发送的超时事件也可以包含参数数据。它也可以使用非默认优先级发送。以下是一些示例:

bool b = true; 
timer.informIn(RTTimespec(5, 0), &b, &RTType_bool);

设置一个一次性定时器,在当前时间的 5 秒后到期(相对时间)。定时器超时事件将带有一个布尔参数,并且在定时器超时时产生的定时器消息将为此参数设置为 true。请注意,超时事件的 UML 定义没有任何参数。只有在设置定时器的时候,我们才能指定它是否应该具有参数。因此,如果我们想要它具有参数,我们还必须指定此参数的类型。这就是为什么在调用 informIn() 时,我们必须将布尔类型的类型描述符作为最后一个参数传递的原因。

bool b = true; 
timer.informIn(RTTimespec(5, 0), RTTypedValue(&b, &RTType_bool), High); 

与上述相同,但是使用 RTTypedValue 对象来指定参数数据和类型。超时事件将以高优先级级别发送。
所有设置定时器的函数都会返回一个 RTTimerNode 对象,从中您可以构造一个 RTTimerId 对象。如果要取消定时器,则需要存储 RTTimerId 对象。取消定时器确保它不会产生超时消息。即使在取消定时器的时间点上,定时器已经过期,这也是正确的。在这种情况下,超时消息存在于消息队列中,并等待从那里发送到capsule实例。当取消定时器时,超时消息将从消息队列中删除。以下是一个示例:

RTTimerId tid = timer.informIn(RTTimespec (10, 0)); 
if (!tid.isValid()) 
log.log("error when setting a timer"); 
else 
timer.cancelTimer(tid); 

设置一个一次性定时器,在现在的10秒后超时。然后,使用获取的 RTTimerId 对象立即取消定时器。请注意,如果设置定时器失败,则可能会得到 null 值而不是 RTTimerNode 对象,在这种情况下,RTTimerId 将变得无效。
通常情况下,定时器会在另一个转换中被取消,通常是因为在定时器过期之前到达某个特定消息。在这种情况下,请确保将 RTTimerId 对象存储在capsule的属性中,而不是在转换本地变量中。还请注意,RTTimerId 对象仅在以下情况下有效:

  1. 定时器的超时消息已分派(对于定期定时器不适用),或
  2. 定时器已被取消
    如果有疑问,可以对RTTimerId对象使用isValid()函数来检查计时器id是否有效。
    请注意,RT服务库为已设置的计时器维护指向RTTimerId对象的指针。这意味着您必须确保RTTimerId对象的地址不会更改,否则这些指针将无效。例如,将RTTimerId对象存储在容器中是不安全的,因为容器不能保证指向所包含对象的指针在添加或从容器中删除对象时保持有效(这种“不安全”容器的一个例子是std::vector)。如果需要使用这样的容器来跟踪多个计时器,则应该只存储指向RTTimerId对象的指针,而不是对象本身。
    当capsule实例被销毁时,所有活动的定时器都会自动取消。

Frame Service

帧服务通过协议帧(Frame)提供,可以用来为模型中的端口分类。该服务允许您以各种方式动态地处理capsule实例,例如:

  • 添加或删除capsule实例到/从可选和插件capsule部件
  • 从capsule部件访问capsule实例
  • 获取有关UML模型的运行时表示的信息,例如其结构和某些运行时对象的属性
    Frame协议没有任何事件,因此所有功能都是以实现Frame协议的 RT Services Library 的C++类的函数形式公开的。这个类是Frame::Base.

Working with Optional Capsule Parts

帧服务允许您通过调用incarnate()函数将新的capsule实例化到可选capsule部件中。capsule部件由一个RTActorRef对象标识。您可以从对应的capsule部件的成员变量中获取此对象。

默认情况下,capsule部分的类型用于确定要实例化的capsule。但是,只要该capsule与作为capsule部分类型的capsule兼容,就可以指定实例化另一个capsule。
调用incarnate()函数允许您传递capsule例的初始化数据。这些数据可以通过名为rtdata的参数在其初始转换中由capsule实例访问。如果调用incarnate()的线程是将运行capsule实例化的capsule的线程,则capsule的初始转换将同步运行(阻塞调用者)。在这种情况下,如果需要向调用者返回一些信息,可以修改初始化数据(前提是您已取消选中“初始转换的Const rtdata参数”复选框)。但是,如果线程不同,则初始转换将基于调度初始化事件异步运行,在这种情况下,不应修改rtdata。

如果需要传递在创建capsule实例时已经可用的初始化数据,需要定义自定义capsule构造函数,然后使用incarnateCustom()函数。此函数允许您提供代码(以lambda表达式的形式),该代码将调用“new”运算符,其中可以将初始化数据传递给构造函数。有关自定义capsule构造函数的更多信息,请参阅RSARTE文档中的“自定义capsule构造函数”文章。

默认情况下,实例化的capsule实例开始在与调用者相同的逻辑线程中运行。但是,在实例化时,您可以指定新的capsule实例将在另一个逻辑线程中运行。您可以使用转换配置编辑器中Threads选项卡中指定的名称来引用可用的逻辑线程。

如果capsule部分被复制(即其多重性的上限大于1),您可能还需要指定要插入创建的capsule实例的索引。索引从0开始。如果您将索引指定为-1,RT Services库将在可用的第一个插槽中插入新的capsule实例。这是在capsule部分中的capsule实例被销毁后最后一个可用的插槽。当所有这样的插槽都被填满后,下一个可用的插槽是具有最低索引号的插槽。例如,假设您首先将一个capsule实例实例化到索引0、1、2、3中的一个复制的capsule部分中,然后以相同的顺序销毁capsule实例(0、1、2、3)。然后,如果您开始使用索引-1来实例化capsule实例,那么capsule实例将按以下索引顺序插入:3、2、1、0、4、5、6…

incarnate()(和incarnateCustom())函数返回一个RTActorId对象,该对象是对已实例化capsules实例的句柄。在实例化capsules时,可能会出现许多错误,因此您应始终通过调用isValid()函数来检查此句柄的有效性。以下是一些导致capsules实例化失败和使RTActorId对象无效的原因示例:

  • 您指定了错误的索引(例如,一个大于capsules部分多重性上限的索引)。
  • 复制的capsules部分中的所有索引均已被capsules实例占用,因此没有其他capsules实例可以插入。
  • 实例化的capsules与capsules部分的类型不兼容。
  • 使用的RTActorRef对象与被实例化capsules拥有的可选capsules部分不对应。
  • 没有足够的内存可用于分配新的capsules实例。
    如果实例化失败,您可以使用getError()函数(在RTActor中定义)获取有关失败原因的信息。
    让我们看一个使用Frame服务来化身capsules的示例。
    在这里插入图片描述

capsules“TopCap”具有一个多重性为0…5且由另一个capsules “Cap”定义类型的capsules part “c1”。下面是“TopCap”可能执行的一些代码示例,用于将“Cap” capsules实例化到“c1”中:

RTActorId id = frame.incarnate(c1); 
if (!id.isValid()) { 
RTController::Error error = getError(); 
context()->perror(context()->strerror()); 
}
// 创建一个 id 变量并调用 frame 的 incarnate 方法,将 c1 作为参数传入,将 id 赋值为执行后的返回值
RTActorId id = frame.incarnate(c1); 

// 如果 id 无效
if (!id.isValid()) { 
    // 创建一个 error 变量,调用 getError 方法获得错误信息
    RTController::Error error = getError(); 
    // 在上下文中调用 strerror 方法获得错误消息并输出
    context()->perror(context()->strerror()); 
} 

将“Cap”实例化到“c1”的第一个可用索引处。如果实例化失败,则获取错误代码并打印错误消息。

int data = 14; 
RTActorId id = frame.incarnate(c1, EmptyActorClass, new_RTTypedValue( 
data));

这段代码的含义为:

  1. 定义一个整型变量data,并将其赋值为14。
  2. 使用c1这个自定义capsules,在当前RTFrame中进行实例化。
  3. 在实例化时,使用EmptyActorClass作为capsules类,表示没有特定的用户定义构造函数。
  4. 使用new_RTTypedValue函数创建一个RTTypedValue对象,该对象的值为之前定义的整型变量data。
  5. 将创建的RTTypedValue对象作为参数传递给incarnate函数,以便对capsules实例进行初始化。
  6. 函数返回值为RTActorId类型的实例标识符,可以用于稍后访问、管理和控制新实例化的capsules。

将“Cap”实例化到“c1”的第一个可用索引处。一个整数值(14)作为初始化数据传递,该“Cap”实例可以在其初始转换中获取该值。

RTActorId id = frame.incarnateCustom(c1, 
RTActorFactory([this](RTController * c, RTActorRef * a, int index) { 
return new Cap_Actor(c, a, true); // User-defined constructor 
}) 
);

这段代码的含义是:

  1. 使用c1这个自定义capsules,在当前RTFrame中进行实例化。
  2. 在实例化时,使用给定的RTActorFactory创建capsules实例。
  3. RTActorFactory需要一个函数,该函数输入参数为RTController指针、RTActorRef指针和int类型的索引值,输出为一个指向新实例化的capsules实例的指针。
  4. 在这个例子中,该函数被定义为一个lambda表达式,它接收一个RTController指针和RTActorRef指针以及一个布尔值true作为参数,并返回一个指向Cap_Actor类的新实例的指针。这是一个用户定义的构造函数,该构造函数需要RTController指针、RTActorRef指针以及一个用于初始化的布尔值参数。

将“Cap”实例化到“c1”的第一个可用索引处。在调用为“Cap”定义的自定义capsules构造函数时,将一个布尔值(true)作为初始化数据传递。

RTActorId id = frame.incarnate(c1, EmptyActorClass, (const void*) 0, 
(const RTObject_class*) 0, MyThread, 3); 

将“Cap”实例化到“c1”的第三个索引处。不传递任何初始化数据。实例化的capsules实例将在逻辑线程“MyThread”中运行。
用于销毁capsules实例的函数称为destroy()。该函数有两个重载版本,一个接受capsules实例(即RTActorId对象),另一个接受capsules部分(即RTActorRef对象)。后者的函数版本将销毁capsules部分中存在的所有capsules实例。
这段文字在说明销毁capsules实例的函数destroy()。它有两个重载版本:

  1. 接受capsules实例(RTActorId对象),用来销毁指定的capsules实例。
  2. 接受capsules部分(RTActorRef对象),用来销毁capsules部分中的所有capsules实例。

其中,capsules部分指的是一个节点中的所有capsules实例集合,可以理解为一个容器。

一个节点在实例化时会对应一部分capsules实例。如果我们想要销毁一个特定的capsules实例,可以使用第一种重载版本;如果我们想要将节点中的所有capsules实例都销毁,可以使用第二种重载版本。
这是一个例子:

RTActorId id = frame.incarnationAt(c1, 3); 
if (id.isValid()) { 
if (!frame.destroy(id)) { 
context()->perror(context()->strerror()); 
} 
} 

获取capsules部分"c1"中索引为3的capsules实例。如果这样的capsules实例存在并且是有效的,将会被销毁。

Working with Plugin Capsule Parts

可选capsules部分允许您在运行时决定它们应该包含什么capsules实例。然而,一旦将capsules实例化为可选capsules部分,它将会在该位置保留其整个生命周期。有时您可能需要capsules实例在不同的capsules部分之间移动的灵活性,甚至同时属于多个capsules部分。这可以通过插件capsules部分实现。

这段话主要讲述了关于可选capsules部分的限制,它们在一旦实例化后就无法更改所收纳的capsules实例。如果需要capsules实例在不同的capsules部分之间灵活移动,则需要使用插件capsules部分来实现。插件capsules部分允许capsules实例属于多个capsules部分,从而实现更大范围的灵活度。

框架服务允许您通过调用 import() 函数将现有的capsules实例导入到插件capsules部分中。capsules部分通过一个 RTActorRef 对象来标识。您可以从对应capsules部分的成员变量中获取此对象。

在将capsules实例导入插件capsules部分时,有一些规则需要遵守:

  • capsules实例必须是有效的。您可以使用 isValid() 函数来检查。
  • capsules实例的类型必须与capsules部分的类型兼容。
  • capsules实例不能有已经绑定的端口。

如果违反了这些规则,导入(import())函数将失败并显示一个错误代码。您可以使用 RTActor 中定义的 getError() 函数来获取有关其失败原因的信息。如果插件capsules部分被复制(即其多重性的上限大于1),则需要指定插入导入capsules实例的索引位置。索引从0开始计数。要从插件capsules部分中移除capsules实例,请调用 deport() 函数。现在,让我们看一个使用框架服务将capsules实例导入和移出插件capsules部分的示例。
在这里插入图片描述

capsule “TopCap"具有可选的capsules部分"c0"和两个插件capsules部分"c1"和"c2”。所有capsules部分均由capsules"Cap"进行类型定义。"TopCap"capsules可以包含以下代码:

RTActorId id = frame.incarnate(c0); 
if (!frame.import(id, c1)) { 
context()->perror("Failed to import into c1!"); 
} 
if (!frame.deport(id, c1)) { 
context()->perror("Failed to deport from c1!"); 
} 
if (!frame.import(id, c2)) { 
context()->perror("Failed to import into c2!"); 
}

将一个"Cap" capsules实例化到"c0"中,然后将其导入到"c1"中。之后,端口"p"将绑定到端口"Pa"上。因此,该capsules实例不能立即被导入到"c2"中,而必须先从"c1"中移除。

以上代码实现了将一个"Cap"capsules实例化到"c0"中,然后将其导入到"c1",绑定了端口’p’到端口’Pa’,接着将该capsules实例从"c1"中导出,最后将其导入到"c2"中。

具体来说,首先通过调用框架实例的incarnate()函数将一个"Cap"capsules实例化到"c0"中,并返回所创建实例的唯一标识符。然后通过调用框架实例的import()函数将该capsules实例导入到"c1"中,如果导入失败则输出错误信息并退出。接着,通过调用框架实例的deport()函数将该capsules实例从"c1"中导出,如果导出失败则输出错误信息并退出。最后,通过调用框架实例的import()函数将该capsules实例导入到"c2"中,如果导入失败则输出错误信息并退出。

需要注意的是,由于在"c1"中已经绑定了端口’p’到端口’Pa’上,所以在导入到"c2"之前必须将其从"c1"中导出。否则,在导入到"c2"时可能会因为端口重名而导致capsules实例化失败。

Accessing Model Information at Run-Time

描述实时应用程序的UML模型包含一些静态的设计时信息,有时在运行时访问这些信息非常有用。例如,您可能想要访问一个capsules的名称,以便编写一个通用的日志记录函数。然而,UML模型的某些部分也具有运行时表示,其中包含动态信息。例如,通过RTActorRef对象实现capsules部件的运行时表示,并且您可能需要访问动态信息,如当前复制系数(即capsules实例在capsules部件中存储的数量)。
框架服务提供了几个函数,允许您访问静态(设计时)和动态(运行时)模型信息。
函数incarnationAt()允许您从特定索引处的capsules部分访问capsules实例。就像inventown()一样,返回一个RTActorId对象,您应该使用它的isValid()函数来确定在指定的索引处是否真的有一个有效的capsules实例。还有一个函数incarnationsOf(),它返回当前存在于capsules部件中的所有capsules实例。如果您想在所有capsules实例上进行迭代,这将非常有用。
函数classOf()接受一个capsules实例(RTActorId),并返回该capsules的类型描述符(RTActorClass)。也就是说,该函数获取capsules实例的动态类型。该类型不一定与capsules实例所在的capsules部件的静态类型相同,但动态类型至少应与静态类型兼容。函数className()从其类型描述符返回capsules名称,classIsKindOf()可以用来检查一个capsules是否与另一个capsules相同或是其子类。还有两个有用的函数me()和myClass(),它们返回正在执行的线程执行的capsules实例和capsules。

以下是使用其中一些函数的示例:

RTActorId id = frame.incarnationAt(c1, 3); 
const RTActorClass& cls = frame.classOf(id); 
log.log(frame.className(cls)); 
const RTActorClass& myCls = frame.myClass(); 
if (frame.classIsKindOf(myCls, cls)) 
log.log("Compatible"); 
else 
log.log("Not compatible"); 
log.commit(); 

从capsules部件“c1”的索引3获取一个capsules实例,并获取该capsules实例的类型描述符。将此实例的capsules名称打印到日志中。然后获取正在运行的capsules的类型描述符,并检查这个capsules是否与capsules实例的类型相同或是其子类。最后,刷新所有已打印的日志消息,以便它们出现在控制台上。
// 从部件c1的索引3获取capsules实例的id

RTActorId id = frame.incarnationAt(c1, 3);

// 获取capsules实例id对应的类型描述符
const RTActorClass& cls = frame.classOf(id);

// 打印capsules实例的名称
log.log(frame.className(cls));

// 获取正在运行的capsules的类型描述符
const RTActorClass& myCls = frame.myClass();

// 检查是否正在运行的capsules是与capsules实例类型相同或者其子类
if (frame.classIsKindOf(myCls, cls)) {
    // 如果是相同或者其子类,打印信息到日志
    log.info("The running capsule is the same or a subclass of the capsule instance type.");
} else {
    // 如果不是相同或者其子类,打印信息到日志
    log.info("The running capsule is not the same or a subclass of the capsule instance type.");
}

// 刷新所有已打印的日志消息,以便它们出现在控制台上
flushLog();

Exception Service

异常服务通过一个名为“Exception”的协议提供,该协议可用于对模型中的端口进行类型标记。该服务允许您向这些端口发送异常事件,以表明出现了异常情况。通常情况下,这可能是代码本身无法处理的错误情况。异常消息与任何其他消息一样,由RT服务库进行处理,即它将被放置在接收capsules实例的消息队列中。当异常消息被调度时,可能会触发一个转换,该转换可以提供必要的操作来处理异常情况。

实现Exception协议的RT Services Library的C++类是Exception::Base,它包含了用于引发一些预定义异常的函数,旨在覆盖常见的错误情况。这些函数返回一个RTExceptionSignal对象,该对象提供了一个raise()函数,用于将异常事件发送到异常端口。

以下是引发异常的代码示例(“exPort”是Exception协议类型的一个端口):

exPort.userError(RTString("An error occurred.")).raise();

在端口“exPort”上引发异常“userError”并使用一个字符串作为参数数据来构造异常消息。
每个异常都由Exception协议中的特定内部事件表示。这些内部事件的UML定义未指定任何事件参数,但是您可以使用RTTypedValue对象实际上通过这些事件传递任何数据。在上面的示例中,我们使用了这个可能性,通过“userError”异常事件传递了一个字符串。使用异常事件传递参数数据是可选的,如下面的示例所示:

exPort.error().raise();

在端口“exPort”上引发异常“error”,异常消息不包含任何参数数据。
可能会出现以下例外情况:

  • arithmeticError:算术错误
  • error:一般错误
  • notFoundError:未找到错误
  • notUnderstoodError:未理解错误
  • serviceAccessError:服务访问错误
  • streamError:流错误
  • subclassResponsibilityError:子类责任错误
  • timeSliceError:时间切片错误
  • userError:用户自定义错误
    请注意,RT Services Library 本身不会引发任何异常。因此,您的应用程序负责引发需要处理的异常。这也意味着,没有精确的定义何时使用特定类型的异常。您的应用程序必须定义在何时引发某种异常的条件。一般的“error”异常可以用于那些其他异常名称无法描述清楚的错误情况。

如果没有可用转换来触发已发送的异常消息,那么这将被视为其他消息一样处理(参见消息传递)。当然,应用程序中使用异常是可选的。您始终可以决定以不涉及异常的其他方式处理错误。例如,您可以定义自己的错误处理协议,或者使用其他机制处理错误。然而,在RT Services Library调用的代码中使用C++异常(throw/catch)通常是不合适的,因为通常不可能捕获抛出的异常。如果您确实使用C++异常,请确保在控制权返回RT Services Library之前捕获它们。

External Port Service

外部端口服务是通过协议External提供的,可用于为模型中的端口设定类型。该服务允许您从与RT Services Library和从UML模型生成的代码不相关的线程发送事件到这些端口。外部端口是将由RSARTE生成的代码与实时应用程序的外部代码集成的有用机制。例如,您可能有一个应用程序的部分负责对外部刺激作出反应,例如从套接字读取的字节或从传感器读取的数据。这样的代码可能在一个外部线程中运行,并通过外部端口与从UML模型生成的代码交互。

实现 External 协议的 RT Services Library 的 C++ 类是 External::Base,它包含了一些函数,可以启用或禁用端口以接收这些外部事件。这些函数必须从capsules实例所在的线程中调用,即只有在capsules实例准备好接收此类事件时,才能向其发送外部事件。以下是在capsules实例上运行的代码示例,用于启用和禁用外部端口“extPort”上的事件接收:

extPort.enable();

启用外部端口上的事件接收。这使端口处于可以接收一个外部事件的模式。

extPort.disable();

禁用在外部端口上接收事件。这会使端口处于模式在那里它不能接收任何外部事件。
当外部端口接收到一个事件后,它会自动变为禁用状态。为了能够接收另一个事件,它必须重新启用。
以下是仅在外部线程中执行的代码示例,它将向外部端口“extPort”发送一个事件:

if (extPort.raise() == 0){ 
//fail 
} 
else { 
//pass 
} 

向外部端口发送事件。如果事件发送失败,raise()函数将返回零。失败的典型原因是外部端口当前未启用以接收外部事件。
发送到外部端口的事件可以包含任何数据。以下是使用触发的事件发送字符串的示例:

char* str = "external data"; 
extPort.raise(&str, &RTType_RTpchar); 

外部数据是在通过 External 协议调用的事件触发的状态转换中接收的。以下是接收上述发送的字符串的状态转换代码示例:

RTpchar p = *((RTpchar*) rtdata); 
printf("Received external data: %s", p);

有时,外部线程中的数据可能以比capsules能够(或者希望)处理的速度更快的速度变得可用。在这种情况下,将数据传递到capsules线程中的 raise() 函数调用时不方便。外部线程必须维护一个存储接收到的数据的数据结构,直到capsules准备好接收它。在 raise() 函数调用中发送一个完整的数据结构需要定义一个自定义类型描述符以避免复制。

这段话主要是说,有时外部线程发来的数据量可能比capsules应用处理的速度要快,不能直接通过 raise() 函数传递数据到capsules线程中,因为这样需要维护一个缓存数据的数据结构,防止发生数据丢失。如果想在 raise() 函数中发送完整的数据结构,还需要定义一个自定义类型描述符来避免数据拷贝和数据丢失等问题。所以,对于大量数据的传输,需要谨慎考虑capsules应用的处理能力和外部线程的数据发送速度,以保证数据传输的准确性和完整性。

外部端口服务提供了一种更方便的数据传输机制,在这种情况下更加合适。外部线程可以直接调用外部端口上的操作,将任意数据对象推送到外部端口本身上。可以推送任意数量的数据对象到外部端口上(即不必一个一个地处理它们)。以下是一个存储包含字符串和整数的一对数据的示例:

std::pair<std::string,int>* data = new std::pair<std::string,int>("external 
data", 15); 
extPort.dataPushBack(data); 

当适当时,外部线程可以通过调用 raise() 函数而不传递任何参数来通知capsules应用有外部数据可用。如果调用失败,外部线程可以选择等待一段时间后再次尝试,或简单地忽略它,并在更多的外部数据可用之前不再调用 raise()。应该根据capsules应用通知外部数据可用的紧急程度来决定应该怎么做。
当capsules应用准备处理外部数据时,它可以选择是否处理所有接收到的数据,或者只处理其中的一部分。它也可以选择按照它们到达的顺序(FIFO)处理数据,或者首先处理最近接收到的数据(LIFO)。哪种方法最好取决于应用程序。以下是一个示例,其中它以 FIFO 的方式处理上述示例中接收到的所有数据:

unsigned int remaining; 
do { 
std::pair<std::string,int>* data; 
remaining = extPort.dataPopFront((void**) &data); 
if (data == 0) 
break; 
// Handle received external data here... 
delete data; 
} 
while (remaining > 0);
unsigned int remaining; // 声明 remaining 变量,表示还剩多少数据待处理
do {
    std::pair<std::string,int>* data; // 声明 data 变量,表示一对字符串和整数的数据
    // 从外部端口队列中取出数据并强制转换为 data 指针,并返回队列中剩余数据数
    remaining = extPort.dataPopFront((void**) &data);
    if (data == 0) // 如果 data 指针为空,即无数据可处理时,跳出循环 
        break;
    // 在这里处理接收到的外部数据...
    delete data; // 处理完毕后释放内存
}
while (remaining > 0); // 如果外部端口队列中还有数据待处理,继续循环

请注意,外部线程为外部数据分配内存,而capsules则负责在处理完外部数据后将其删除。
有时您可能会选择创建专门的数据结构来存储外部数据。例如,您可能想要避免向capsules发送重复数据,或者对数据结构有其他更特定的要求。在这种情况下,外部线程和capsules线程必须以线程安全的方式访问共享数据。例如,您可以使用互斥锁来保护它。不同平台都提供了互斥锁的实现,详情请参考:

<InstallDir>/rsa_rt/C++/TargetRTS/src/target/<target>/RTMutex.h.

Dependency Injection Service

TargetRTS通过RTInjector类提供了简单的依赖注入服务。您可以单独使用此服务,也可以与C++依赖注入框架结合使用,以在实时应用程序中实现依赖注入。

请注意,这里的“依赖关系”指的是某种形式的运行时依赖关系,并非用于表示元素之间的编译时依赖关系的UML依赖关系概念。运行时依赖关系的示例包括:在具体化capsules部件时使用哪个capsules,将什么配置数据传递给其capsules构造函数以及在哪个线程中运行具体化的capsules实例。

依赖注入服务与capsules工厂(参见RTActorFactory)密切相关。capsules工厂可以将创建capsules实例的请求委托给依赖注入服务。这使得将应用程序中所有capsules的依赖项集中在一个中心位置(例如顶层capsules构造函数)成为可能。
为了理解依赖注入服务提供的价值,让我们首先看一个没有使用它的例子。假设我们有一个 LogSystem capsules,其内部使用“logger”capsules部件实现一些日志记录功能:
在这里插入图片描述

该应用程序可能具有多个日志记录功能的实现,由从 AbstractLogger 继承的不同子capsules表示。例如:
在这里插入图片描述

现在假设您有时想使用一种日志记录实现,有时想使用另一种。也许您甚至想根据某些运行时条件动态地从一种实现切换到另一种实现。
如果没有依赖注入,就必须编写代码来决定在实例化“logger”capsules部件时使用 AbstractLogger 的哪个子capsules。如果capsules部件被声明为可选的,这种代码将是对 RTFrame :: incarnate()的调用,而如果它被声明为固定的,您将提供代码作为capsules工厂中的“Create Function Body”代码片段,例如在“logger”capsules部件上。这两种方法的问题是,应用程序将包含分散在应用程序中不同位置的硬编码“配置”代码。要更改应用程序配置以使用不同的日志记录实现,必须找到所有这样的配置代码并对其进行更改。
这段文字解释了在没有依赖注入的情况下,你需要写很多额外的代码来决定应该使用哪个日志记录实现。如果你的代码需要在运行时切换日志记录实现,你会在应用程序的不同地方散布硬编码的配置代码。为了改变应用程序的配置,你需要找到所有这些代码,并单独修改它们,这会导致代码不易维护和修改。
通过依赖项注入,可以将这样的配置代码放在一个地方,可能在应用程序逻辑本身之外。例如,您可以在顶部capsules构造函数中编写代码,该构造函数注册一个用于化身“记录器”capsules部分的创建函数:

RTInjector::getInstance().registerCreateFunction("/logger:0/logger", 
[this](RTController * c, RTActorRef * a, int index) { 
//return new SimpleLogger_Actor(c, a); 
return new TimestampLogger_Actor(LoggerThread, a); 
} 
); 

capsules部分通过其完全限定的运行时名称(与模型调试器使用的名称相同)进行标识。请注意,依赖注入不仅让我们配置创建实例的capsules,还让我们配置它应该在哪个线程中运行,以及传递给其capsules构造函数的任何初始化数据。
请注意,RTInjector类提供了一个可以在应用程序的任何地方使用的单例对象。您既可以使用此单例为capsules部件注册创建函数(如上面的示例),也可以从capsules工厂创建capsules实例。例如,这里有一个简单的capsules工厂实现,它委托RTInjector单例来创建capsules实例:

#include <RTInjector.h> 
class CapsuleFactory : public RTActorFactoryInterface { 
public: 
RTActor* create(RTController *rts, RTActorRef *ref, int index) override { 
return RTInjector::getInstance().create(rts, ref, index); 
} 
void destroy(RTActor* actor) override { 
delete actor; 
} 
static CapsuleFactory factory; 
};

如果您需要在运行时动态更改配置,只需为相同capsules部件注册另一个“create function”,它将覆盖先前注册的“create function”。

Structure of Generated C++ Code

本章提供了有关从RSARTE模型生成的C++代码结构的信息。生成的C++代码广泛使用RT服务库提供的类。

Type Descriptors

类型描述符是为模型中的每个用户定义类型(例如UML类)生成的元数据。RT服务库使用类型描述符中的信息来了解如何初始化、复制、销毁、编码和解码相应类型的对象。
您可以通过“生成描述符”属性来自定义类型的描述符生成方式。此属性在 Properties 视图中的“C++ Target RTS”选项卡中提供。
在这里插入图片描述

如果将此属性设置为“false”,则不会为该类型生成类型描述符。这意味着您无法通过事件将此类型的对象按值发送(因为RT服务库不知道如何复制它)。另一个丢失的功能是使用 Log 服务打印该类型对象的能力(因为RT服务库不知道如何编码它)。如果想为该类型编写自定义类型描述符,则可以将此属性设置为“手动”。包含类型描述符的文件必须与应用程序链接,否则将出现链接错误。有关类型描述符中存储的所有详细信息(以及如果您编写自己的描述符,则必须提供的信息)请参阅 RTObject_class 类。

这段话讲述了在 C++ 中如何使用 RT Services 库实现动态切换日志记录服务。在类型的属性设置中,可以设置“Generate Descriptor”属性来控制是否生成类型描述符。如果此属性设置为“False”,则不会生成该类型的类型描述符,因此无法将该类型的对象按值发送,也无法使用 Log 服务记录该类型的对象。如果需要自定义类型描述符,则可以将属性设置为“Manual”,并编写自定义的类型描述符。但需要注意,编写的类型描述符必须和应用程序链接,否则会出现链接错误。对于自定义类型描述符需要提供的所有信息,可以参考RTObject_class 类的文档。

如果将此属性设置为“True”,则模型编译器将使用“Generate Descriptor”属性下方的函数体代码片段生成类型描述符。请注意,必须提供所有函数体,除了 Move Function Body,如果您想使该类型可移动,则需要实现 Move Function Body(有关可移动类型的示例,请参见“避免复制消息数据”)。

请注意,在许多情况下,模型编译器可以生成默认的类型描述符实现,如果您的需求被该默认实现覆盖,则无需实现任何函数体代码片段。

每个类型描述符在生成的应用程序中仅实例化一次,并且是常量。以下是为“MyClass”类生成的类型描述符常量示例:

extern const RTObject_class RTType_MyClass; 

当您需要将类型描述符传递给RT服务库中的函数时,应使用为类型描述符生成的常量的地址。
RT Services 库包含所有基本类型的预定义类型描述符。约定是对于类型 T,类型描述符称为 RTType_T。例如,类型描述符 RTType_double 是 double 类型的类型描述符。

RT Services 库中的一些函数需要传入数据值和数据类型两个参数。数据值通常以未类型化指向数据的指针(void*)的形式表示,而数据类型使用类型描述符(RTObject_class*)表示。为了能够将数据值和类型描述符封装在一个单独的对象中,RT Services 库提供了一个名为 RTTypedValue 的类。

capsules的名称,其超级capsules的类型描述符的引用以及有关capsules端口的信息。这些信息被 RT Services 库中的各种函数使用,让您能够在运行时访问模型信息。请参阅“在运行时访问模型信息”。

这段文本主要讲述了在创建capsules时,使用 RTActorClass 类对象生成一种特别的类型描述符,用来描述capsules的信息,比如名称、超级capsules的类型描述符引用和capsules端口的信息等。这些信息可以被 RT Services 库中的某些函数获取,以便在运行时访问模型信息。如果想深入了解这方面的内容,可以参考“在运行时访问模型信息”这篇文档。

Type Descriptor Hints

如上所述,模型编译器可以自动为所有简单类型生成类型描述符。这包括枚举和由其他简单类型类型化的属性的类。但是,它无法为更复杂的类型生成类型描述符。一个常见的情况是,模型中的类被翻译成了 STL 容器类型的 typedefs 或 type aliases,例如 lists、vectors 或 maps。这些类型对于模型编译器来说过于复杂,无法自动生成类型描述符。但是,有一项首选项“实时开发 - 构建/转换 - C++ - 为复杂类型生成类型描述符”,可以设置让模型编译器尝试为这些类型生成类型描述符。为此,您需要指定“类型描述符提示”属性,以便模型编译器可以了解有关该类型的一些信息。以下指令在此属性中得到识别:

@kind=vector|map|list|set
STL 容器类型
• @itemType=identifier|qualifiedIdentifier
STL 容器的项目类型 (对于 list/set/vector)。如果该类型是一个嵌套类型,您应该使用它的完全限定名称,例如 MyClass::MyNestedClass。
• @keyType=identifier|qualifiedIdentifier
• @dataType=identifier|qualifiedIdentifier
STL 容器的键值/数据类型 (对于 maps)。 例如,假设有一个 typedef 类型 std::map<A,B> 或类似的 type alias 的实现类。那么这个 typedef(或 type alias)的“类型描述符提示”属性应包含以下行:
@kind=map
@keyType=A
@dataType=B

这段话解释了如何在“类型描述符提示”属性中使用指令来告诉模型编译器关于 STL 容器类型的信息。对于 vector、list 和 set,我们需要指定 @kind=vector|list|set 和 @itemType=identifier|qualifiedIdentifier。@itemType 指定了容器中元素的类型,如果该类型是嵌套类型,则应使用完全限定名称来指定。对于 map,还需要指定 @keyType 和 @dataType,分别指定 map 中键值对的键和值的类型。例如,如果有类似于 std::map<A,B> 的 type alias,那么在该 alias 的“类型描述符提示”属性中应该包含以下指令:
@kind=map
@keyType=A
@dataType=B
这样,模型编译器就可以理解该类型是一个 map 类型,以及 A 和 B 分别是其键和值的类型。

Templates

与 typedef 不同,C++ 中的类型别名可以具有模板参数。在这种情况下,生成的类型描述符也将具有相同的模板参数。这确保了对于类型别名的每个具体实例化,都将有相应的类型描述符实例可用。

生成的类型描述符函数将作为模板函数生成,这使得能够通用实现它们,因此它们可以在不考虑实际使用的模板参数的情况下工作。如果需要,您可以专门为某些实际模板参数集使用特殊实现来特化这些模板函数。

当为具有模板参数的类型别名实现通用类型描述符时,通常很有用能够查找实际使用的模板参数类型的类型描述符。TargetRTS 提供了一个名为 RTObject_class::fromType() 的模板函数可用于此目的。例如,假设我们定义了以下类型别名:
这段话讲的是在实现通用的类型描述符时,可能会用到一个函数 RTObject_class::fromType(),可以通过该函数查找使用实际模板参数的类型的类型描述符。这段话还给出了一个类型别名的例子来说明如何使用这个函数。

template<typename T, unsigned int N > using StdArray = std::array<T, N>;

然后,我们可以实现该类型别名的编码函数,让它能够编码所有类型的数组,无论实际使用的元素类型是什么:

template<typename T, unsigned int N > inline int rtg_StdArray_encode( const 
RTObject_class * type, const StdArray< T, N > * source, RTEncoding * coding ) 
{ 
//{{{USR 
platform:/resource/type_descriptor_with_template_parameter/CPPModel.emx#_gcMGALo_E 
eu4j48Uy6dLVQ|Target RTS|encodeFunctionBody 
const RTObject_class *elementTypeDescriptor = RTObject_class::fromType<T>(); 
if (!elementTypeDescriptor) 
return 0; // Element type descriptor not available 
int sum = 0; 
bool first = true; 
sum += coding->write_string(type->name()); 
sum += coding->write_string("{"); 
for (auto i = source->begin(); i != source->end(); i++) { 
if (!first) 
sum += coding->write_string(","); 
first = false; 
T element = *i; 
sum += elementTypeDescriptor->encode(&element, coding); 
} 
sum += coding->write_string("}"); 
return sum; 
//}}}USR 
} 

template<typename T, unsigned int N>
inline int rtg_StdArray_encode(const RTObject_class *type, const StdArray<T, N> *source, RTEncoding *coding) {
    //{{{USR 
    // 在此处添加用户自定义代码
    // 定义一个指针,指向存储元素类型 T 的类型描述符
    const RTObject_class *elementTypeDescriptor = RTObject_class::fromType<T>();
    if (!elementTypeDescriptor)
        return 0; // 元素类型描述符不可用,直接返回 0
    int sum = 0;
    bool first = true;
    sum += coding->write_string(type->name()); // 将类型名称写入编码流
    sum += coding->write_string("{");
    // 遍历数组中的所有元素
    for (auto i = source->begin(); i != source->end(); i++) {
        if (!first)
            sum += coding->write_string(",");
        first = false;
        T element = *i;
        // 对每一个元素,调用其类型描述符的 encode 函数进行编码
        sum += elementTypeDescriptor->encode(&element, coding);
    }
    sum += coding->write_string("}");
    // 返回编码数据长度
    return sum;
    //}}}USR 
}

注释中简要说明了函数的作用和实现,首先通过 RTObject_class::fromType<T>() 查找存储元素类型 T 的类型描述符,针对数组中的每个元素,调用元素存储类型的类型描述符的 encode 函数进行编码,最后返回编码数据的长度。

TargetRTS 为每个内置的 C++ 类型提供了 fromType<T>() 模板函数的实现。这些模板特化可以在 RTObject_class.h 文件中找到。要使此函数也适用于其他类型,如用户定义的类型或 TargetRTS 提供的类型,您需要为它们编写类似的特化。例如:

template <> inline const RTObject_class* RTObject_class::fromType<StdString>() { 
return &RTType_StdString; } // User-defined type: StdString 
template <> inline const RTObject_class* RTObject_class::fromType<RTString>() { 
return &RTType_RTString; } // Type provided by the TargetRTS: RTString 

生成的代码将为类型描述符定义一个名称,默认情况下该名称与模板的所有实例共享。它设置为类型别名的名称。对于上面的示例:

template<typename T,unsigned int N> const char* RTName_StdArray<T,N>::name = "StdArray"; 

您可以特化此变量定义以为您使用的模板的每个实例使用更具体的名称来命名类型描述符。例如:

template <> const char* RTName_StdArray<StdString, 4>::name = "StdArray<StdString, 4>"; 
template <> const char* RTName_StdArray<RTString, 2>::name = "StdArray<RTString, 2>"; 

如果您不这样做,并且模板被实例化多次,则会存在多个具有相同名称的类型描述符。这是允许的,但是运行时会显示警告。例如:

WARNING: A type "StdArray" was already installed

有一个名为RTObject_class::lookup()的函数,它可以根据类型名称返回一个类型描述符。如果存在多个具有相同名称的类型描述符,则此函数将失败,因此建议为每个类型描述符设置一个唯一的名称。

Threads

RT服务库的一个优点是更改生成的实时应用程序的线程配置非常容易。这主要是通过将实际使用的物理线程与概念上的逻辑线程分开实现的。

Logical threads and physical threads

每个capsules实例都有自己的逻辑控制线程。这意味着从概念上讲,它在自己的线程中运行,独立于其他capsules实例。然而,变换配置编辑器允许您创建多个逻辑线程(在“线程”选项卡中),每个线程都可以运行多个capsules实例。
逻辑线程在变换配置编辑器的“线程”选项卡中被映射到真实的物理线程。这种映射允许您控制应用程序中物理线程的总数,以及每个物理线程控制的capsules实例数量。

在生成的代码中,每个逻辑线程都作为RTController类的变量可用,并根据“线程”选项卡中指定的名称进行命名。这个变量在生成的C++代码中被分配到相应的物理线程。这意味着当您在应用代码中引用逻辑线程时,您实际上可以访问由RTController对象表示的相应物理线程。

将逻辑线程映射到物理线程的函数称为_rtg_mapLogicalThreads(),并生成到单元C++文件中(默认为UnitName.cpp)。在这个函数旁边,您还将找到生成的函数_rtg_createThreads()和rtg_deleteThreads(),其中包含创建和删除已添加的物理线程的代码,除了默认的MainThread和TimerThread。

下面是一个示例,展示如何在变换配置编辑器中的“线程”选项卡中添加一个额外的物理线程“CustomThread”,以及一个逻辑线程“MyThread”:
在这里插入图片描述

当您实例化可选的capsules部分时,您可以指定新的capsules实例应在您在“线程”选项卡中指定的逻辑线程中运行。顶级capsules实例始终在MainThread中执行,每个包含在固定capsules部分中的capsules实例始终在与所有者capsules实例相同的线程中执行。有关在自定义逻辑线程中运行化身capsules实例的示例,请参阅使用可选capsules部分。

您所选择的逻辑线程和物理线程之间的映射方式对应用程序性能具有重要影响。可能执行长时间任务的capsules实例有益于在单独的物理线程中运行,因为当一个capsules实例执行任务时,映射到同一物理线程的所有其他逻辑线程都必须等待,直到正在运行的capsules实例重新将控制返回到RT服务库(参见运行至完成语义)。但是同时,目标环境可支持的物理线程数量存在实际限制,您必须确保应用程序保持在这些限制之内。
这句话的意思是,当我们配置逻辑线程和物理线程的映射关系时,选择的方式会影响应用程序的性能。一些执行时间较长的任务所对应的capsules实例会对性能造成特别大的影响。为了避免这种情况,我们可以将这些capsules实例放在单独的物理线程中运行,并确保其他跟该capsules实例映射到同一物理线程的逻辑线程都不会阻塞等待。但是,我们需要注意的是,物理线程数量是有限制的,因此我们必须确保应用程序的线程数量不超过该限制。

某些目标环境只支持一个线程。在这种情况下,当编译生成的C++代码时,预处理器宏USE_THREADS将不会被设置。这将会移除所有处理多线程的代码,例如函数_rtg_createThreads()和rtg_deleteThreads()等。

要访问表示正在执行的线程的RTController对象,可以调用RTActor类中可用的context()函数。 RTController类提供了一些有用的函数,例如用于访问线程中发生的最近错误的函数。以下是一个使用一些RTController函数的示例:

RTController* c = context(); 
if (c->getError() != RTController::ok) { 
c->perror("An error occurred on thread "); 
log.log(c->name()); 
c->abort(); 
} 

// 获取执行线程的RTController对象
RTController* c = context(); 

// 如果线程中有错误发生
if (c->getError() != RTController::ok) {
    
    // 输出错误信息到屏幕
    c->perror("An error occurred on thread "); 
    
    // 记录线程名到日志文件中
    log.log(c->name()); 
    
    // 中止线程运行
    c->abort(); 
    
}

检查上下文线程是否存在最近的错误。如果有错误,则打印错误信息,然后打印上下文物理线程的名称(在转换配置编辑器的"线程"选项卡中指定)。最后,终止上下文线程(这将销毁该线程运行的所有capsules实例)。

Inside the C++ RT Services Library

本章介绍了关于RT服务库工作原理的一些重要知识,以及您的应用程序可以利用的一些可选实用程序。

Run-to-Completion Semantics

RT服务库不会抢占capsules处理。RT服务库的核心是由物理线程运行的控制器对象,它负责将消息分派给它管理的capsules实例。控制器对象有一个消息队列,用于存储针对任何受控的capsules实例的消息。控制器对象的基本操作模式是从其消息队列中取出下一个消息并将其传递到目标capsules以进行处理。当它传递消息时,将调用目标capsules的状态机来处理消息。

这句话的意思在RT服务库中,capsules实例是在逻辑线程上执行的实体,因此需要通过将逻辑线程和物理线程进行映射来管理它们。物理线程是由操作系统分配的资源,而逻辑线程是由应用程序开发人员分配和控制的。在RT服务库中,开发人员需要通过适当的配置将逻辑线程映射到物理线程上,并在化身capsules实例中使用逻辑线程进行多线程并发操作。正确的线程映射方式和capsules实例数量对应物理线程数目的重要性需要开发人员注意,特别是在实际场景中需要考虑物理线程数目的限制。RT服务库还提供了一些可选的实用程序,例如RTController类的函数可以获取最近线程错误,以方便开发人员进行调试。同时,开发人员还需要注意RT服务库的完成语义,以便正确处理消息并提高应用程序的性能。

在capsules的触发转换完成处理消息并运行完毕之前,控制权不会返回给RT服务库。每个capsules实例一次只处理一个消息。它一直处理当前消息直到转换链的完成(转换链可以由几个代码片段组成,如一个守卫条件、一个退出操作、一个转换效果、一个选择点条件和一个进入操作),然后将控制权返回给RT服务库等待下一个要分派的消息。这种方案称为运行至完成语义。通常,涉及转换链的代码片段应该短小且快速完成,以便迅速处理消息。

这句话的意思是在RT服务库中,capsules实例只有在处理完当前的消息转化链之后才会将控制权归还给RT服务库,等待下一个消息的到来。为了保证快速处理消息,capsules实例一次只能处理一个消息,而转换过程中涉及的各个代码片段 (如保护条件、退出动作、转换效果、选择点条件和进入动作) 应该尽量短小精悍,快速完成执行。这种方案被称为“运行到完成”语义。开发人员需要注意,保持转换链步骤的简洁和快速性,以提高应用程序的性能,同时正确处理消息以避免出现悬挂和死锁等问题。

Intra-thread and Inter-thread Communication

了解线程内和线程间的通信机制以及如何使用RT服务库处理消息非常重要。

从应用程序的角度来看,在线程内发送消息和跨线程发送消息没有区别;发送消息的代码仍然相同。然而,也存在一些性能问题,跨线程发送消息的速度大约慢10-20倍。因此,优化设计将彼此之间具有强烈消息通信的capsules实例放置在同一物理线程上。

RT服务库实现了简单的消息调度算法:查找可用控制器对象中最高优先级的非空消息队列。然后从该队列的队头取出消息,并将其传递给接收方capsules。一旦接收方capsules处理完消息,就会重复执行该过程。

消息通过调用 rtsBehavior(signal, port) 进行传递。该调用是在消息调度循环内进行的。因为在capsules实例完成执行并进行 “return” 后控制才会返回循环,所以强制执行了“运行到完成”语义。

Message queues

每个物理线程都有自己的RTController对象,带有自己的消息队列。更确切地说,这是一个消息队列数组,每个优先级级别都有一个。控制器对象将其所有消息组织成两种类型的队列:internalQ和incomingQ。

InternalQ 包含可以由其控制器调度的消息。在同一物理线程上具有不同化身的capsules之间传递的消息(即同一RTController对象上的消息)将被放置到 internalQ 中。下面是一个示例,显示在特定时间点,当有 7 条消息准备好进行调度时,internalQ的样子。
在这里插入图片描述

从图片可以看出,队列根据优先级分类,以确保具有更高优先级的消息在具有较低优先级的消息之前被调度。

IncomingQ 用于线程之间的通信。它保存来自其他物理线程的消息(即其他RTController对象)。来自 incomingQ 的消息将在控制器循环的适当时机首先移动到 internalQ 中,然后由控制器以与其他内部消息相同的方式进行调度。IncomingQ 的组织方式与 internalQ 相同:

RTMessageQ internalQ[ OTRTS_NUMPRIO ]; 
RTMessageQ incomingQ[ OTRTS_NUMPRIO ]; 

Message structure and freeList of messages

消息队列中的每个消息都是 RTMessage 类的对象,包含一个信号 ID (即协议事件的 ID)、优先级、数据指针、有关端口和接收器的信息、标志以及大小为 RTMESSAGE_PAYLOAD_SIZE 的数据区。

如果要发送的数据适合于消息正文数据区,则会将其简单地复制到消息中。如果数据不适合负载区域,则会在系统堆中分配数据内存,并在消息被接收后释放。数据区的大小(RTMESSAGE_PAYLOAD_SIZE)是可配置的,默认大小为100个字节。

每个 RTController 对象都有一个已分配的 RTMessage 对象池,这些对象没有数据,并且在需要发送新消息时使用。这个池被称为“空闲列表”:

RTMessage * freeList 

资源管理器对象(RTResourceMgr)在启动时分配初始的 freeList 消息对象队列。当通过低阈值时,资源管理器将分配额外的消息对象,并在通过高阈值时将这些额外的消息对象返回给消息池。
在这里插入图片描述

当一个线程需要发送消息时,资源管理器从相应的 RTController 对象的 freeList 中取出下一个空闲消息对象,并将其返回给控制器,控制器会填充所有的消息信息并发送消息。如果列表中没有空闲的消息,则会通过调用函数分配一个新的消息批到系统堆中。

unsigned msgAlloc( RTMessage * &, unsigned howMany = 50U ) 

默认情况下,会添加50个新的消息。需要注意的是,一旦分配了消息对象,它们就不会被释放回系统堆,而是返回到消息池中。

当消息池的大小达到最大空闲列表大小时(默认为100),消息对象将从消息池中被释放,只有最小空闲列表大小(默认为20)的消息对象会保留在消息池中。此操作在 RTController::freeMsg 中执行。

可用队列阈值和数据缓冲区大小是可静态配置的值,如果这些值发生更改,则有必要重新生成RT服务库。

Intra-thread message sending

发送消息时,发送者托管对象会在其控制器上调用 RTController::send 函数,该函数又会在接收者托管对象的控制器上调用 RTController::receive 函数。如果两个托管对象位于同一个物理线程上,也就是运行在同一个控制器上,消息将根据发送语句中指定的优先级附加到适当的 internalQ[message_priority] 消息队列中。当发送者托管对象执行完当前的迁移,控制权将返回到主循环程序(mainLoop),然后调用 RTController::dispatch 函数,该函数将从 internalQ 中调度消息。
在这里插入图片描述

Inter-thread message sending

对于跨线程通信,将在发送者对象所在的控制器/线程上调用 RTController::send,在接收者对象所在的不同控制器/线程上调用 RTController::receive。在这种情况下,RTController::receive 会调用 RTController::peer_receive,该函数会将消息放入接收者控制器对象的 incomingQ 中。peer_receive 函数中的互斥量保护了对 incomingQ 的访问。这确保了向同一接收者线程发送消息的多个线程在访问接收者的 incomingQ 变量期间不会干扰。
在这里插入图片描述

Message dispatch algorithm

每个 RTController 对象都运行自己的 mainLoop,从 incomingQ 中调度消息。在通常情况下,每次调度迭代都会执行以下操作:
• 检查 incomingQ 是否有任何消息,并将所有消息移动到 internalQ 的末尾。
• 从 internalQ 中获取最高优先级的第一条消息,并将其从 internalQ 中删除。
• 通过调用 receiver->rtsBehavior(signal_id, port_id) 将此消息传递给接收者托管对象,这可能会触发接收者托管对象状态机中的迁移。
• 释放该消息。
在这里插入图片描述

在每个调度迭代的开始,来自 incomingQ 的消息会被移动到相应的 internalQ,无论它们的优先级如何。只有在从 internalQ 中选择要传递的消息时才会考虑优先级。
由于对 incomingQ 的访问受到互斥锁保护,因此可以保证在 RTController::acceptIncoming 函数更新队列时,没有外部线程会向 incomingQ 放置任何新的消息。同一互斥锁也保证不同的外部线程不会同时访问 incomingQ。

Encoding and Decoding

RT Services 库可以将消息和数据值编码为文本表示形式。这在许多情况下都非常有用,例如在跟踪实时应用程序中发生了什么信息时(例如使用模型调试器),或者在将事件和数据发送到当前进程之外的位置时(例如实现分布式应用程序或与云服务器通信)。同样,也可以将这样的文本表示形式解码,以获取原始消息或数据值的内存中副本。
通过取消定义宏对象 OBJECT_ENCODE 和 OBJECT_DECODE,可以从 RT Services 库中删除编码和解码支持。编码和解码是通过类型描述符的 encode 和 decode 函数执行的。这些函数定义如下:

typedef int (*RTEncodeFunction)(const RTObject_class * type, 
const void * source, 
RTEncoding * ); 
typedef int (*RTDecodeFunction)(const RTObject_class * type, 
void * target, 
RTDecoding * ); 

这些函数类型中的最后一个参数指定用于执行编码或解码的编码或解码对象。默认的编码/解码实现使用紧凑的 ASCII 表示形式(由类 RTAsciiEncoding 和 RTAsciiDecoding 实现)。例如,在使用模型调试器进行跟踪时,您将看到使用此表示形式:
如果需要数据值的文本表示,可以手动调用encode函数。例如,以下是用于对接收到的消息的数据进行编码并将其打印到stdout的代码:

const RTObject_class* mt = msg->getType(); 
char buf[1000]; 
RTMemoryOutBuffer buffer( buf, 1000 ); 
RTAsciiEncoding coding( &buffer ); 
mt->_encode_func(mt, msg->getData(), &coding); 
buffer.write("", 1); // IMPORTANT: Terminate the buffer string before printing it! 
std::cout << "ASCII encoding: " << buf << endl << flush; 

// 获取消息类型
const RTObject_class* mt = msg->getType();

// 创建一个长度为1000的字符数组作为缓冲区
char buf[1000];

// 创建一个输出缓冲区对象,它会将编码后的数据写入到 buf 中
RTMemoryOutBuffer buffer( buf, 1000 );

// 创建一个 ASCII 编码对象,用于将数据编码成紧凑的ASCII表示形式
RTAsciiEncoding coding( &buffer );

// 对消息数据进行编码,写入到输出缓冲区中
// 注意,传入的编码对象为 ASCII 编码对象
mt->_encode_func(mt, msg->getData(), &coding);

// 将空字符串写入缓冲区作为字符串的结尾,这一步很重要
buffer.write("", 1);

// 打印编码后的 ASCII 表示形式
std::cout << "ASCII encoding: " << buf << endl << flush;

注释中提供了对代码中每行语句的简要说明,方便理解。

这里假设编码后的字符串长度不超过1000个字符,因此在对数据进行编码时使用了固定大小的内存缓冲区。如果您不想对编码后的字符串长度做任何假设,可以考虑使用 RTDynamicStringOutBuffer 类,该类实现了动态字符串缓冲区。

除了默认的ASCII编码外,RT Services库还实现了另外两种编码方式:
• 版本控制的ASCII编码
这是通过 RTVAsciiEncoding 实现的,与 RTAsciiEncoding 完全相同,只是包括了类型描述符的版本号。
• JSON编码
这是通过 RTJsonEncoding 实现的。将数据编码为JSON格式可以在将数据发送到Web服务器或其他实时应用程序时非常有用,因为JSON是一种标准的数据格式。请注意,目前没有相应的解码实现来处理JSON编码。

JSON编码的API与ASCII编码相同,但还提供了一个函数来对整个消息对象进行编码。以下是一个示例,使用上述提到的RTDynamicStringOutBuffer将接收到的消息编码为JSON格式:

RTDynamicStringOutBuffer buf; 
RTJsonEncoding coding(&buf); 
coding.put_msg(msg); 
cout << "Received msg: " << buf.getString() << endl << flush;

// 创建一个动态字符串输出缓冲区
RTDynamicStringOutBuffer buf;

// 创建一个JSON编码器并将其与缓冲区相关联
RTJsonEncoding coding(&buf);

// 使用编码器将消息对象编码为JSON格式,并把结果存储到缓冲区
coding.put_msg(msg);

// 输出接收到的消息
cout << "Received msg: " << buf.getString() << endl << flush;
上面跟踪示例中显示的事件的JSON编码如下所示:
{"event" : "event_with_class", 
"type" : "MyClass", 
"data" : {"a" : 8, "b" : false} 
} 

默认情况下,RTJsonEncoding和RTDynamicStringOutBuffer均未被包含,因此如果您想使用它们,必须显式地从RT Services库中包含这些文件。如果您需要将消息或数据值编码/解码为另一种表示形式,则可以创建一个继承自RTEncoding和/或RTDecoding的自定义类。这些类包含可以重写的虚拟函数,以实现自定义的编码和/或解码。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。