零拷贝是 VLink 在数据路径上施加的一类非侵入能力:削减大负载在收发链路上的内存复制,使相机帧、点云、栅格地图、张量、目标列表、音频等传感器数据的搬运延迟与带宽消耗降至最低。它遵循统一的 URL 契约(传输后端与 URL、QoS 配置)——业务代码仍只面对六个通信原语,能力以"领域容器作为消息类型 T"或"借贷接口 loan()"的形式接入,调用方式不因启用而改变。零拷贝优化的是序列化前后的内存搬运,与序列化之后插入的安全加密管线正交,可在同一端点叠加。
大负载传感器数据(相机帧、点云、栅格地图、张量、目标列表、音频)在通信链路中反复复制,会成为延迟与带宽的主要来源。VLink 通过两条互相独立、可叠加的机制消除这类复制,二者作用于数据路径的不同阶段:
- 传输层零拷贝(loan):
shm:///shm2://(以及显式开启共享内存的zenoh://)允许发布端从共享内存池借出缓冲区并就地写入,订阅端经指针收到同一块内存,避免一次内核拷贝。作用于跨进程搬运阶段。 - 容器层零拷贝:
vlink::zerocopy命名空间下的领域容器在反序列化时使内部指针直接指向接收缓冲区,负载数据不被复制。作用于解码阶段,与后端无关。
两层的组合效果取决于后端:
| 后端 | 容器层借用 | 传输层 loan | 数据路径 |
|---|---|---|---|
shm:// / shm2:// |
是 | 是 | 双层零拷贝 |
zenoh://?shm=1 |
是 | 是 | 双层零拷贝 |
dds:// / intra:// 等 |
是 | 否 | 仅容器层借用 |
传输层 loan 的机制与配置见 6.10,完整后端参数见 传输后端与 URL。
每个领域容器都作为 Publisher<T> / Subscriber<T> 的消息类型 T 使用,按数据语义选择。选定容器后,收发即为常规的发布/订阅用法(见 通信模型)。
| 容器 | 适用数据 | 典型后端 | 结构图 |
|---|---|---|---|
CameraFrame |
图像帧 / 编码图像 / 编码视频 | shm://、dds:// |
camera-frame-structure |
PointCloud |
激光雷达 / 深度点云 | shm://、dds:// |
point-cloud-structure |
OccupancyGrid |
2D 占据 / 代价 / SDF 地图 | shm://、dds:// |
occupancy-grid-structure |
Tensor |
神经网络张量输入/输出 | shm://、dds:// |
tensor-structure |
ObjectArray |
3D 检测 / 跟踪目标列表 | 任意 | — |
AudioFrame |
PCM / 编码音频帧 | 任意 | — |
RawData |
自定义二进制负载 | 任意 | — |
ProxyData |
代理层消息信封及原始载荷 | 任意 | — |
vlink::Publisher<vlink::zerocopy::CameraFrame> pub("shm://camera/front");
vlink::Subscriber<vlink::zerocopy::PointCloud> sub("dds://lidar/points");序列化机制见 消息序列化;
shm/shm2/zenoh传输配置见 传输后端与 URL;Bytes用法见 基础库。完整示例见examples/zerocopy/zerocopy_basic。
七个容器共享同一套元数据、方法与所有权模型。本节集中阐述共性,后续各容器节只列各自特有的字段与枚举。
七个容器(含 RawData)均内嵌一个 Header header 成员(40 字节、序列化时随负载一并写入),承载序列号、坐标系标识与时间戳。常用四字段:
| 字段 | 类型 | 含义 |
|---|---|---|
header.frame_id |
char[16] |
坐标系 / 传感器标识(可不带终止符,读取用 header.frame_id_view()) |
header.seq |
uint32_t |
单调递增序列号,在 UINT32_MAX 处回绕,用于检测丢帧 |
header.time_meas |
uint64_t |
采集时间戳(自 UNIX 纪元起的纳秒) |
header.time_pub |
uint64_t |
发布时间戳(自 UNIX 纪元起的纳秒) |
time_pub - time_meas 为采集到发布的处理延迟;订阅端用「接收时刻 − time_pub」估算传输延迟。两个时间戳均为「自 UNIX 纪元起的纳秒」,应用层用标准库取得:
// 返回自 UNIX 纪元起的纳秒时间戳,供 header 填写
static uint64_t now_ns() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
}后续各容器示例中省略其余 header 赋值,发布前按下例填写:
frame.header.seq = seq++;
frame.header.time_meas = capture_ts_ns;
frame.header.time_pub = now_ns();| 方法 | 语义 |
|---|---|
create(size) |
分配并拥有 size 字节缓冲区,随后写入 data() |
data() / size() |
只读负载指针 / 负载字节数 |
shallow_copy(ptr, size) |
借用外部指针,不复制数据(仅五个字节容器) |
shallow_copy(other) |
借用同类对象的缓冲区,并复制其 header |
deep_copy(ptr, size)(仅五个字节容器)/ deep_copy(other) |
深拷贝,得到独立的拥有缓冲区 |
move_copy(other) |
转移所有权,other 随后失效 |
is_valid() |
负载非空且有效 |
is_owner() |
是否拥有当前缓冲区,决定析构是否释放 |
check_valid(bytes) |
静态方法,校验一段 Bytes 是否为本容器的合法负载 |
clear() |
释放拥有的缓冲区并归零字段 |
PointCloud 与 ObjectArray 为变长记录容器,接口有所不同:PointCloud 以 Schema 创建(create_v3f<...>(count, names) 等)、size() 返回点数、负载指针为 get_internal_data();ObjectArray 以 create(count) 预分配、count() 返回记录数、objects(i) 取记录。这两个容器只提供同类对象的 shallow_copy(other) / deep_copy(other),没有裸指针 (ptr, size) 重载。其余五个容器(CameraFrame、OccupancyGrid、Tensor、AudioFrame、RawData)适用上表的 create(size) / data() / size() 字节语义,并同时提供 (ptr, size) 与 (other) 两种借用/拷贝重载。
发布与订阅容器时序列化由框架自动完成,应用层无需手工编解码。仅在手工落盘或自定义网络收发时才需关注序列化细节。
内存所有权由创建方式决定,经 is_owner() 区分:
| 创建方式 | is_owner() |
析构行为 |
|---|---|---|
create(size) / deep_copy(...) |
true |
释放缓冲区 |
shallow_copy(...) |
false |
不释放 |
move_copy(other) |
继承 other |
取决于源 |
借用(shallow_copy)时容器仅持有指针,源缓冲区的生存期必须覆盖容器的生存期,否则 data() 悬空。边界条件见 6.11。
头文件 include/vlink/zerocopy/camera_frame.h。携带分辨率、像素格式、通道、采集频率等元数据与像素缓冲区,同时支持原始像素(YUV/RGB/Mono/Bayer/OpenCV 数值格式)与编码帧(JPEG/PNG/WebP/MJPEG/H.26x/AV1)。
常用元数据 setter:set_width(w)、set_height(h)、set_format(fmt)、set_channel(ch)、set_freq(hz)、set_stream(s)(仅编码视频)。对应 getter 为去掉 set_ 前缀的同名方法。
- 像素格式
Format:原始格式kFormatYuv420、kFormatNv12、kFormatNv21、kFormatYuyv、kFormatBgr888Packed、kFormatRgb888Packed、kFormatRgb888Planar、kFormatMono8、kFormatMono16、kFormatRgba8888Packed、kFormatBgra8888Packed,通用 OpenCV/ROS 数值格式kFormatUint8C1到kFormatFloat64C4,Bayer 格式kFormatBayerRggb8到kFormatBayerGrbg16;编码格式kFormatJpeg、kFormatPng、kFormatMjpeg、kFormatH264、kFormatH265、kFormatH266、kFormatAv1、kFormatWebp。 - 编码名转换:
CameraFrame::format_from_encoding("32FC1")可从 ROS/OpenCV/codec 名称得到Format;encoding_from_format(fmt)返回规范编码名。 - 视频流帧类型
Stream(编码视频):kStreamI(关键帧)、kStreamP(前向预测帧)、kStreamB(双向预测帧)。
static constexpr uint32_t kW = 1920;
static constexpr uint32_t kH = 1080;
vlink::Publisher<vlink::zerocopy::CameraFrame> pub("shm://camera/front");
pub.wait_for_subscribers();
vlink::zerocopy::CameraFrame frame;
frame.header.time_pub = now_ns();
frame.set_width(kW);
frame.set_height(kH);
frame.set_format(vlink::zerocopy::CameraFrame::kFormatNv12);
frame.create(kW * kH * 3 / 2);
camera_driver_fill(const_cast<uint8_t*>(frame.data()), frame.size());
pub.publish(frame);vlink::Subscriber<vlink::zerocopy::CameraFrame> sub("shm://camera/front");
sub.listen([](const vlink::zerocopy::CameraFrame& frame) {
if (!frame.is_valid()) {
return;
}
const uint8_t* plane = frame.data();
process(plane, frame.width(), frame.height());
});编码视频流:发布端经 shallow_copy(data, size) 借用编码器输出缓冲区,并以 set_format(kFormatH264)、set_format(kFormatH265)、set_format(kFormatH266) 或 set_format(kFormatAv1) 与 set_stream(...) 标注;订阅端按 format() / stream() 路由至解码器。
头文件 include/vlink/zerocopy/point_cloud.h。点云容器自带字段 Schema(字段名与类型),收发两端无需额外协议约定即可解读每个点的各字段。最常用的是 v3f 系列(XYZ 以 float 存储):
| 方法 | 语义 |
|---|---|
create_v3f<ExtraT...>(count, names) |
创建 XYZ + 附加字段的点云,预留 count 个点 |
push_value_v3f(x, y, z, extras...) |
追加一个点 |
get_value_v3f(index) |
读第 index 个点的 XYZ,返回 Vector3f |
get_key_map() + get_value<T>(index, key_map, "field") |
按字段名读任意字段 |
size() 返回当前点数,pack_size() 返回单点字节数。需要双精度坐标时改用 create_v3d / push_value_v3d / get_value_v3d。
vlink::zerocopy::PointCloud pc;
pc.header.time_pub = now_ns();
pc.create_v3f<float>(100000, {"intensity"});
for (size_t i = 0; i < 100000; ++i) {
pc.push_value_v3f(x[i], y[i], z[i], intensity[i]);
}
pub.publish(pc);sub.listen([](const vlink::zerocopy::PointCloud& pc) {
if (!pc.is_valid()) {
return;
}
auto key_map = pc.get_key_map();
for (size_t i = 0; i < pc.size(); ++i) {
vlink::zerocopy::PointCloud::Vector3f p = pc.get_value_v3f(i);
float intensity = pc.get_value<float>(i, key_map, "intensity");
consume(p.x, p.y, p.z, intensity);
}
});海量点云可按需启用三种带宽优化,三者互不依赖:
| 优化 | 接口 | 效果 |
|---|---|---|
| 精度量化 | create_v3f(count, names, extent) |
传入坐标绝对值上界 extent,XYZ 按 int16_t 量化存储(约省一半带宽),get_value_v3f 读取时自动反量化;坐标落在 (-extent, +extent) 之外的点在 push_value_v3f 时被直接丢弃(非饱和) |
| 垂直排布 | set_vertical(true) |
序列化负载按字段成列(SoA),更利于下游熵编码;内存布局不变 |
| 体素降采样 | downsample(level)(level 取 1..255) |
在已量化(extent>0)且拥有缓冲区(owned)的点云上将空间相近点折叠为每体素一个,原地缩减点数;借用/反序列化得到的点云无法降采样 |
头文件 include/vlink/zerocopy/occupancy_grid.h。按行主序存储 width × height 个同质单元格,内嵌世界坐标变换、阈值、地图 ID 等元数据,语义对齐 ROS nav_msgs/OccupancyGrid 并支持更高位宽的单元格类型。
常用 setter:set_width(w)、set_height(h)、set_resolution(米/格)、set_origin_x/y/z/yaw(...)、set_cell_type(t)、set_default_value(v)、set_occupied_threshold(t) / set_free_threshold(t)、set_map_id("name")。
- 单元格类型
CellType:kCellInt8(ROS 风格-1 / 0..100)、kCellUint8(0..254 代价图,255 表示 unknown)、kCellUint16(高分辨率代价图)、kCellFloat32(概率 / 对数几率 / SDF)。cell_size()返回单元格字节数。 (origin_x, origin_y)为第 0 行 0 列单元格的左下角,地图绕原点旋转origin_yaw弧度(遵循 REP-103)。
vlink::zerocopy::OccupancyGrid og;
og.header.time_pub = now_ns();
og.set_map_id("global");
og.set_width(400);
og.set_height(400);
og.set_resolution(0.05F);
og.set_origin_x(-10.0F);
og.set_origin_y(-10.0F);
og.set_cell_type(vlink::zerocopy::OccupancyGrid::kCellInt8);
og.set_default_value(-1);
const size_t cells = static_cast<size_t>(og.width()) * og.height() * og.cell_size();
og.create(cells);
std::memset(const_cast<uint8_t*>(og.data()), 0, cells);
pub.publish(og);头文件 include/vlink/zerocopy/tensor.h。最多 8 维,同时存形状 shape 与步长 strides,可无损往返非连续视图(如 NCHW 切片)。适用于神经网络输入/输出、特征图、语言模型隐藏态等。
常用 setter:set_shape(shape, rank)(同步推导 strides 与 num_elements)、set_dtype(t)、set_layout("NCHW")、set_device(d)、set_name(sv)、set_model_id(sv)。量化张量另填 set_quant_scale / set_quant_zero_point。
- 元素类型
DataType:kInt8、kUint8、kInt32、kInt64、kFloat16、kBfloat16、kFloat32、kFloat64等。element_size()返回单元素字节数。 - 设备提示
Device:kDeviceCpu、kDeviceGpu、kDeviceNpu、kDeviceDsp。
vlink::zerocopy::Tensor t;
t.header.time_pub = now_ns();
t.set_name("image");
t.set_layout("NCHW");
t.set_dtype(vlink::zerocopy::Tensor::kFloat32);
t.set_device(vlink::zerocopy::Tensor::kDeviceGpu);
uint32_t shape[] = {1, 3, 224, 224};
t.set_shape(shape, 4);
t.create(t.num_elements() * t.element_size());
fill_input(const_cast<uint8_t*>(t.data()), t.size());
pub.publish(t);头文件 include/vlink/zerocopy/object_array.h。变长数组容器,每个 Object 记录一个障碍物的姿态、尺寸、运动学、分类与跟踪 ID。Object 字段全部公开,可直接读写。
- 容器方法:
create(count)预分配count个槽位、push_value(obj)追加、objects(i)取第i项只读指针、count()当前数量;元数据set_source_id("name")、set_channel(c)。 Object常用字段:label[32](类别名)、position[3]、size[3]、yaw、velocity[3]、acceleration[3]、score(置信度)、class_id、track_id、motion_state、source_type。- 运动状态
MotionState:kMotionStationary、kMotionMoving、kMotionStopped、kMotionParked。 - 来源传感器
SourceType:kSourceLidar、kSourceCamera、kSourceRadar、kSourceFusion、kSourceUltrasonic。
vlink::zerocopy::ObjectArray arr;
arr.header.time_pub = now_ns();
arr.set_source_id("fusion_v2");
arr.create(256);
vlink::zerocopy::ObjectArray::Object obj;
std::strncpy(obj.label, "car", sizeof(obj.label) - 1);
obj.position[0] = 12.0F;
obj.size[0] = 4.5F;
obj.yaw = 0.1F;
obj.score = 0.92F;
obj.track_id = 42;
obj.motion_state = vlink::zerocopy::ObjectArray::kMotionMoving;
obj.source_type = vlink::zerocopy::ObjectArray::kSourceFusion;
arr.push_value(obj);
pub.publish(arr);订阅端遍历:for (uint32_t i = 0; i < arr.count(); ++i) { const auto* o = arr.objects(i); ... }。
头文件 include/vlink/zerocopy/audio_frame.h。传输一段原始 PCM 或编码音频,适用于麦克风采集、TTS 输出、车机语音、车载娱乐音频流等。
常用 setter:set_sample_rate(hz)、set_num_channels(c)、set_num_samples(n)、set_format(f)、set_layout(l)、set_codec("PCM")、set_language("zh")(供语音识别使用)。
- 采样/编码格式
Format:kFormatPcmS16、kFormatPcmS24、kFormatPcmS32、kFormatPcmF32、kFormatPcmU8、kFormatOpus、kFormatAac、kFormatMp3、kFormatFlac。 - 通道布局
Layout:kLayoutInterleaved(L,R,L,R...)、kLayoutPlanar(各通道独立平面)。
vlink::zerocopy::AudioFrame frame;
frame.header.time_pub = now_ns();
frame.set_sample_rate(48000);
frame.set_num_channels(2);
frame.set_num_samples(960);
frame.set_format(vlink::zerocopy::AudioFrame::kFormatPcmS16);
frame.set_layout(vlink::zerocopy::AudioFrame::kLayoutInterleaved);
frame.set_codec("PCM");
const size_t payload = frame.num_samples() * frame.num_channels() * sizeof(int16_t);
frame.create(payload);
capture_pcm(const_cast<uint8_t*>(frame.data()), payload);
pub.publish(frame);头文件 include/vlink/zerocopy/raw_data.h。最简容器,仅封装一个 Header 与一段无类型字节缓冲区,适合承载自定义协议结构体。经 create(size) 分配后写入 data(),订阅端按约定结构体解读;亦可经 shallow_copy(ptr, size) 借用外部缓冲区直接零拷贝发布。
struct MyProtocol {
uint32_t cmd;
uint32_t flags;
float payload[256];
};
vlink::zerocopy::RawData rd;
rd.header.time_pub = now_ns();
rd.create(sizeof(MyProtocol));
auto* proto = reinterpret_cast<MyProtocol*>(const_cast<uint8_t*>(rd.data()));
proto->cmd = 0x1001;
pub.publish(rd);sub.listen([](const vlink::zerocopy::RawData& rd) {
if (!rd.is_valid() || rd.size() < sizeof(MyProtocol)) {
return;
}
const auto* proto = reinterpret_cast<const MyProtocol*>(rd.data());
handle(proto->cmd);
});代理层另有内部容器 ProxyData(include/vlink/zerocopy/proxy_data.h),供 VLink 代理路由使用,普通应用一般不直接操作。
需要在运行期按序列化类型读取消息的工具和扩展,应包含 <vlink/zerocopy/message_parser.h> 并使用 vlink::zerocopy::MessageParser。解析器统一识别 RawData、CameraFrame、PointCloud、ProxyData、OccupancyGrid、Tensor、ObjectArray 与 AudioFrame 八种类型;CLI、viewer、analyzer、Web 桥接和 Python 绑定的通用字段读取使用这一入口,避免各层重复维护类型识别、边界检查与字段类型转换。为避免热路径回退,viewer 和 Web 可视化中 CameraFrame / PointCloud 的专用实时渲染可以直接调用对应容器 codec,绕过通用字段解析器。各容器 codec 仍是底层序列化实现。
vlink::zerocopy::MessageParser parser;
if (!parser.parse(serialized_type, bytes)) {
return;
}
vlink::zerocopy::MessageParser::Value value;
if (parser.value("header.time_meas", value)) {
consume(value);
}
if (parser.value("data", 3, "track_id", value)) {
consume(value);
}根字段使用点路径,例如 header.frame_id、width、dtype。变长集合通过 value(collection, index, field, out) 读取:PointCloud.data[N].field 与 ObjectArray.data[N].field 读取记录字段,OccupancyGrid.data[N].value 和 Tensor.data[N].value 读取标量,Tensor.shape[N].value / strides[N].value 读取维度信息。collection_size() 提供统一的边界;fields() 与 element_fields() 可用于动态 UI 或 schema 构建。
Field 描述符除字段名与 Value 类型外,还带有呈现语义:enum_kind 标明字段编码的内置枚举(供解析出符号名)、is_time 标明纳秒时间戳、is_bool 标明布尔、is_reserved 标明可隐藏的保留槽。基于这些元数据,同一头文件提供一个纯反射驱动的可读渲染器 format_message(parser, options):它遍历 fields() / element_fields() 生成规范文本,包括 header {}、PointCloud 的 protocol {} 与逐点展开、Tensor shape、枚举符号名、日期、十六进制和布尔;其他二进制集合保持摘要形式。vlink-dump、vlink-efbs、vlink-eproto 共用这一渲染器,输出保持一致。
Value 保留 int64_t / uint64_t 的整数精度。只有调用 numeric(..., double&, &precision_loss) 为 ExprTk 等浮点计算显式转换时,超过 IEEE-754 精确整数范围的值才会通过 precision_loss 报告精度损失。解析得到的容器可能借用输入 Bytes 的存储,因而输入缓冲区必须至少与解析器同寿命,并且在解析器有效期间不得修改其内容、大小、容量或底层存储。Python 的 parse() / parse_type() 会自动保活输入对象;如果检测到输入指针或大小发生变化,后续读取会令该解析结果表现为无效。原地内容修改无法由解析器可靠检测,仍属于调用方禁止操作。
loan 作用于跨进程搬运阶段:发布端从共享内存池借出缓冲区写数据,订阅端收到指向同一块内存的指针。is_support_loan() 仅在 shm://、shm2:// 以及显式开启共享内存的 zenoh://?shm=1 上返回 true;其余后端返回 false,此时 loan() 返回空 Bytes,照常用 publish() 发布即可,仅不享受传输层零拷贝。
| 方法 | 语义 |
|---|---|
is_support_loan() |
查询当前后端是否支持 loan |
loan(size) |
从共享内存池借出 size 字节缓冲区,失败返回空 Bytes |
return_loan(bytes) |
归还借出但未发布的缓冲区 |
set_manual_unloan(true) |
订阅端关闭回调返回后的自动归还 |
vlink::Publisher<vlink::Bytes> pub("shm://camera/raw");
pub.wait_for_subscribers();
if (pub.is_support_loan()) {
vlink::Bytes buf = pub.loan(1920 * 1080 * 3 / 2);
if (!buf.empty()) {
camera_driver_fill(buf.data(), buf.size());
pub.publish(buf);
}
}边界条件:借出后若未 publish(),必须显式 pub.return_loan(buf),否则共享内存池会耗尽。
订阅端默认在回调返回后自动归还 loan。若需在回调外继续持有指针,开启手动模式并自行归还:
vlink::Subscriber<vlink::Bytes> sub("shm://camera/raw");
sub.set_manual_unloan(true);
sub.listen([&](const vlink::Bytes& msg) {
process(msg);
sub.return_loan(msg);
});安全端点(
SecT == kWithSecurity/SecurityPublisher)发布时会跳过传输层 loan——密文长度在加密前未知,框架退回常规序列化路径。容器层借用不受影响,加密管线见 安全加密。
loan 的完整传输配置见 传输后端与 URL。
借用机制(shallow_copy、容器反序列化、loan)只持有指针,不复制数据。两条约束可避免悬空:
约束一:源缓冲区的生存期必须覆盖借用它的容器。 容器反序列化后 data() 指向接收 Bytes 的内部存储,Bytes 必须在容器之后析构。
vlink::Bytes bytes = recv();
vlink::zerocopy::RawData rd;
rd << bytes;
process(rd);上例中 bytes 在 rd 之前声明、在同一作用域内更晚析构,rd.data() 在 process 期间始终有效。若 bytes 先于 rd 离开作用域,rd.data() 即悬空。
约束二:move_copy 后源对象失效。 转移所有权后源对象 is_valid() 为 false,不可再用。shallow_copy(other) 复制 header 但数据仍为借用,源对象同样须更长寿;需要独立缓冲区时改用 deep_copy()。
| 维度 | 裸 Bytes |
zerocopy 容器 |
|---|---|---|
| 元数据 | 无 | 宽高 / 格式 / 形状 / 时间戳 / 类别等 |
| 反序列化拷贝 | 有 | 无(借用指针) |
| 格式校验 | 无 | 有(check_valid) |
| 跨语言互操作 | 需自行约定协议 | 内置 Schema(PointCloud / Tensor) |
| 适用场景 | 通用小消息 | 传感器 / 模型 / 地图 / 检测 / 音频大负载 |
判据:小消息或已有自定义序列化时用 Bytes;传感器领域大负载优先用对应领域容器,并在 shm:// 上叠加传输层 loan 取得双层零拷贝。







