15. ROS 2中的Unicode支持

本文描述了ROS 2将会如何支持使用Unicode标准发送多字节字符数据。本文还描述了如何将此类数据发送到ROS 1桥接上。

作者:克里斯·拉兰塞特(Chris Lalancette)

撰写日期:2019-05

最后修改:2020-07

15.1 背景

一些用户希望发送以某些用ASCII 字符无法表示的语言表示的文本数据。目前ROS 1只支持字符串字段中的ASCII数据,但允许用户用UTF-8填充字符串字段

请注意,话题名称不能使用多字节字符,因为DDS规范不允许使用多字节字符。有关详细信息,请参阅“话题和服务名称(topic and service name)”设计文档。

下列链接包含有关多字节字符和字符编码历史的更多信息。

kunststube.net/encoding

stackoverflow.com/quest

diveintopython3.net/str

stackoverflow.com/quest

utf8everywhere.org/

15.2 字符串中的Unicode字符

ROS 2中字符串的两个目标就是与ROS 1的字符串兼容,并与DDS的wire格式兼容。ROS 1的字符串字段包含ASCII编码数据,但允许使用UTF-8。DDS-XTYPES要求使用UTF-8作为IDL类型string的编码。为了同时与两者兼容,ROS 2中数据类型string的编码预期会是UTF-8。

15.3 宽字符串(Wide Strings)

ROS 2的消息将会有一个新的原始(primitive)字段类型wstring。其目的是允许ROS 2节点使用含有wstring字段的IDL与非ROS的DDS实体进行通信。类型wstring的数据编码应该是UTF-16以匹配DDS-XTYPES 1.2版本。由于UTF-8和UTF-16都可以编码相同的代码点,因此新的 ROS 2消息应该更喜欢使用string类型而不是wstring类型。

15.4 要求编码但不保证强制执行

类型string和wstring的编码要求是UTF-8和UTF-16,但可能不会强制执行此要求。由于ROS 2的目标是资源受限系统,因此会让rmw实现来选择是否强制执行编码。而且,由于许多用户都会编写代码来检查字符串是否包含有效数据,因此在某些情况下可能不需要在底层再次对字符串进行检查。

如果使用错误的编码填充某个string或wstring字段,则其行为就是未定义的。rmw实现可能会允许将无效字符串传递给订阅者。每个订阅者负责发现无效字符串并决定如何处理它们。例如,像ros2 topic echo这样的订阅者可能会以十六进制来回显这些字节。

IDL规范禁止string包含NULL值。为了兼容,ROS消息中string字段不能包含零字节(zero bytes),而wstring字段不能包含零单词(zero words)。这个限制会强制执行。

15.5 跨ROS 1桥接的Unicode字符串

由于ROS 1和ROS 2都允许string类型的编码为UTF-8,因此ROS 1桥接将会在ROS 1和ROS 2之间传递未经修改的值。如果具有字符串字段的消息由于其内容不是合法的UTF-8而序列化失败,那么默认行为将会是丢弃整条消息。像替换无效字节之类的其他策略可能会无意中改变其含义,因此如果完全可用,它们会是选项。

*注:对wstring字段的桥接尚未实现。详细信息请参见ros2/ros1_bridge#203

如果ROS 2消息具有wstring类型的字段,则ROS 1桥接将会尝试将其从UTF-16转换为UTF-8。生成的UTF-8编码字符串将会以string类型发布。如果转换失败,则默认情况下ROS 1桥接就不会发布该消息。

15.6 宽字符串的大小

UTF-8和UTF-16都是可变宽度的编码。为了减少内存量的使用,string和wstring类型都会根据可能的最小代码点存储在客户端库中。这意味着string必须指定为一个字节序列(sequence of bytes),而wstring必须指定为一个单词序列(sequence of words)。

一些DDS实现目前使用32位的类型来存储宽字符串值。这可能是由于DDS-XTYPES 1.1版本第7.3.1.5节将wchar指定为32位值。但是,DDS-XTYPES 1.2版本第7.3.1.4节将其更改为16位值。预计未来大多数DDS实现将会切换到16位字符存储。ROS 2的目标是与DDS-XTYPES 1.2版本兼容,并且会使用16位存储宽字符。在进行消息序列化或反序列化时,为ROS 2消息生成的代码会自动处理这个转换。

15.6.1 有界宽字符串

消息定义可能会限制字符串的最大大小。这些字符串被称为有界字符串。有界字符串的目的是限制使用的内存量大小,因此必须将其界限指定为内存单位。如果某个string字段是有界的,则其大小会以字节为单位。类似地,有界wstring的大小将会以单词为单位。填充有界string或wstring的人有责任确保其仅包含完整代码点。部分代码点与无效代码点无法区分,因此最后代码点不完整的有界字符串不能保证会被发布。

15.7 宽字符串的运行时影响

处理宽字符串会给系统软件带来更大的压力,无论是在运行速度还是代码大小方面。UTF-8和UTF-16都是可变宽度的编码,这意味着代码点可以占用1到4个字节,具体取决于编码。可能需要多个代码点来表示用户感知的单个字符。ROS 2的目标之一就是支持在代码大小和处理器速度方面都受限制的微控制器。在这些设备上可能无法执行一些宽字符串操作,例如在用户感知的某个字符上拆分一个字符串。

然而,无论是否使用宽字符串,整个字符串相等性检查都是一样的。在某个ASCII字符上进一步拆分UTF-8字符串与在某个ASCII字符串上拆分一个ASCII字符是相同的。如果在微控制器上的代码必须进行字符串操作,那么它可以通过在遇到一个大于127的字节时停止处理字符串来断言某个string仅包含ASCII数据。

15.8 对ROS 2用户来说该API是什么样子的?

15.8.1 Python 3

在Python中,str类型会被用于字符串和宽字符串。在分配给某个字段之前,应使用bytes.decode将已知编码的字节转换成str。

示例:

import rclpy
from test_msgs.msg import WStrings

if __name__ == '__main__':
    rclpy.init()
    node = rclpy.create_node('talker')
    chatter_pub = node.create_publisher(WStrings, 'chatter', 1)
    msg = WStrings()
    msg.wstring_value = 'Hello Wörld'
 print('Publishing: "{0}"'.format(msg.wstring_value))
    chatter_pub.publish(msg)
    node.destroy_node()
    rclpy.shutdown()

15.8.2 C++

在C++中,宽字符串wchar_t在不同平台上具有不同的大小(Windows上为2个字节,Linux上为4个字节)。 而在ROS 2中对于宽字符串的字符将会使用char16_t,而对于宽字符串本身则将会使用std::u16string。

示例:

/*
* Note that C++ source files containing unicode characters must begin with a byte order mark: https://en.wikipedia.org/wiki/Byte_order_mark .
* Failure to do so can result in an incorrect encoding of the characters on Windows.
* For an example, see https://github.com/ros2/system_tests/pull/362#issue-277436162
*/
#include <cstdio>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "test_msgs/msg/w_strings.hpp"

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<rclcpp::Node>("talker");
  auto chatter_pub = node->create_publisher<test_msgs::msg::WStrings>("chatter", 10);
  test_msgs::msg::WStrings msg;
  std::u16string hello(u"Hello Wörld");
  msg.wstring_value = hello;
  chatter_pub->publish(msg);
  rclcpp::spin_some(node);
  return 0;
}

*英语原文网址:design.ros2.org/article