VLink  2.0.0
A high-performance communication middleware
💨 6. 零拷贝

零拷贝是 VLink 在数据路径上施加的一类非侵入能力:削减大负载在收发链路上的内存复制,使相机帧、点云、栅格地图、张量、目标列表、音频等传感器数据的搬运延迟与带宽消耗降至最低。它遵循统一的 URL 契约(传输后端与 URLQoS 配置)——业务代码仍只面对六个通信原语,能力以"领域容器作为消息类型 `T`"或"借贷接口 `loan()`"的形式接入,调用方式不因启用而改变。零拷贝优化的是序列化前后的内存搬运,与序列化之后插入的安全加密管线正交,可在同一端点叠加。


⚡ 6.1 零拷贝的两层机制

大负载传感器数据(相机帧、点云、栅格地图、张量、目标列表、音频)在通信链路中反复复制,会成为延迟与带宽的主要来源。VLink 通过两条互相独立、可叠加的机制消除这类复制,二者作用于数据路径的不同阶段:

  • **传输层零拷贝(loan)**:shm:// / shm2://(以及显式开启共享内存的 zenoh://)允许发布端从共享内存池借出缓冲区并就地写入,订阅端经指针收到同一块内存,避免一次内核拷贝。作用于跨进程搬运阶段。
  • **容器层零拷贝**:vlink::zerocopy 命名空间下的领域容器在反序列化时使内部指针直接指向接收缓冲区,负载数据不被复制。作用于解码阶段,与后端无关。

两层的组合效果取决于后端:

后端 容器层借用 传输层 loan 数据路径
shm:// / shm2:// 双层零拷贝
zenoh://?shm=1 双层零拷贝
dds:// / intra://仅容器层借用

传输层 loan 的机制与配置见 6.10,完整后端参数见 传输后端与 URL

6.1.1 容器选型概览

每个领域容器都作为 Publisher<T> / Subscriber<T> 的消息类型 T 使用,按数据语义选择。选定容器后,收发即为常规的发布/订阅用法(见 通信模型)。

容器 适用数据 典型后端 结构图
CameraFrame 图像帧 / 编码视频(H.264/H.265) 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 自定义二进制负载 任意

序列化机制见 消息序列化shm / shm2 / zenoh 传输配置见 传输后端与 URLBytes 用法见 基础库。完整示例见 examples/zerocopy/zerocopy_basic

</blockquote>

🧩 6.2 通用容器接口

七个容器共享同一套元数据、方法与所有权模型。本节集中阐述共性,后续各容器节只列各自特有的字段与枚举。

6.2.1 公共元数据 header

七个容器(含 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();

6.2.2 公共方法

方法 语义
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() 释放拥有的缓冲区并归零字段

PointCloudObjectArray 为变长记录容器,接口有所不同:PointCloud 以 Schema 创建(create_v3f<...>(count, names) 等)、size() 返回点数、负载指针为 get_internal_data()ObjectArraycreate(count) 预分配、count() 返回记录数、objects(i) 取记录。这两个容器只提供同类对象的 shallow_copy(other) / deep_copy(other),没有裸指针 (ptr, size) 重载。其余五个容器(CameraFrameOccupancyGridTensorAudioFrameRawData)适用上表的 create(size) / data() / size() 字节语义,并同时提供 (ptr, size)(other) 两种借用/拷贝重载。

发布与订阅容器时序列化由框架自动完成,应用层无需手工编解码。仅在手工落盘或自定义网络收发时才需关注序列化细节。

6.2.3 所有权与借用

内存所有权由创建方式决定,经 is_owner() 区分:

创建方式 is_owner() 析构行为
create(size) / deep_copy(...) true 释放缓冲区
shallow_copy(...) false 不释放
move_copy(other) 继承 other 取决于源

借用(shallow_copy)时容器仅持有指针,**源缓冲区的生存期必须覆盖容器的生存期**,否则 data() 悬空。边界条件见 6.11


📷 6.3 CameraFrame:图像与编码视频帧

头文件 include/vlink/zerocopy/camera_frame.h。携带分辨率、像素格式、通道、采集频率等元数据与像素缓冲区,同时支持原始像素(YUV/RGB)与编码帧(JPEG/H.264/H.265)。

常用元数据 setter:set_width(w)set_height(h)set_format(fmt)set_channel(ch)set_freq(hz)set_stream(s)(仅编码视频)。对应 getter 为去掉 set_ 前缀的同名方法。

  • 像素格式 Format:原始格式 kFormatYuv420kFormatNv12kFormatNv21kFormatYuyvkFormatBgr888PackedkFormatRgb888PackedkFormatRgb888Planar 等;编码格式 kFormatJpegkFormatH264kFormatH265
  • 视频流帧类型 Stream(仅 H.264/H.265):kStreamI(关键帧)、kStreamP(前向预测帧)、kStreamB(双向预测帧)。
static constexpr uint32_t kW = 1920;
static constexpr uint32_t kH = 1080;
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);
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(nal_data, nal_size) 借用编码器输出的 NAL 缓冲区,并以 set_format(kFormatH264)set_stream(...) 标注;订阅端按 format() / stream() 路由至解码器。


☁️ 6.4 PointCloud:带 Schema 的点云

头文件 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)level1..255在已量化(extent>0)且拥有缓冲区(owned)的点云上将空间相近点折叠为每体素一个,原地缩减点数;借用/反序列化得到的点云无法降采样

🗺️ 6.5 OccupancyGrid:2D 占据与代价地图

头文件 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")

  • 单元格类型 CellTypekCellInt8(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);

🧮 6.6 Tensor:N 维神经网络张量

头文件 include/vlink/zerocopy/tensor.h。最多 8 维,同时存形状 shape 与步长 strides,可无损往返非连续视图(如 NCHW 切片)。适用于神经网络输入/输出、特征图、语言模型隐藏态等。

常用 setter:set_shape(shape, rank)(同步推导 stridesnum_elements)、set_dtype(t)set_layout("NCHW")set_device(d)set_name(sv)set_model_id(sv)。量化张量另填 set_quant_scale / set_quant_zero_point

  • 元素类型 DataTypekInt8kUint8kInt32kInt64kFloat16kBfloat16kFloat32kFloat64 等。element_size() 返回单元素字节数。
  • 设备提示 DevicekDeviceCpukDeviceGpukDeviceNpukDeviceDsp
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);

🚗 6.7 ObjectArray:3D 检测与跟踪目标列表

头文件 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]yawvelocity[3]acceleration[3]score(置信度)、class_idtrack_idmotion_statesource_type
  • 运动状态 MotionStatekMotionStationarykMotionMovingkMotionStoppedkMotionParked
  • 来源传感器 SourceTypekSourceLidarkSourceCamerakSourceRadarkSourceFusionkSourceUltrasonic
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); ... }


🔊 6.8 AudioFrame:音频帧(PCM / 编码)

头文件 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")(供语音识别使用)。

  • 采样/编码格式 FormatkFormatPcmS16kFormatPcmS24kFormatPcmS32kFormatPcmF32kFormatPcmU8kFormatOpuskFormatAackFormatMp3kFormatFlac
  • 通道布局 LayoutkLayoutInterleavedL,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);

📦 6.9 RawData:自定义二进制负载

头文件 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);
});

代理层另有内部容器 ProxyDatainclude/vlink/zerocopy/proxy_data.h),供 VLink 代理路由使用,普通应用一般不直接操作。


🚀 6.10 传输层 loan

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


⏳ 6.11 生命周期约束

借用机制(shallow_copy、容器反序列化、loan)只持有指针,不复制数据。两条约束可避免悬空:

约束一:源缓冲区的生存期必须覆盖借用它的容器。 容器反序列化后 data() 指向接收 Bytes 的内部存储,Bytes 必须在容器之后析构。

vlink::Bytes bytes = recv();
vlink::zerocopy::RawData rd;
rd << bytes;
process(rd);

上例中 bytesrd 之前声明、在同一作用域内更晚析构,rd.data()process 期间始终有效。若 bytes 先于 rd 离开作用域,rd.data() 即悬空。

约束二:move_copy 后源对象失效。 转移所有权后源对象 is_valid()false,不可再用。shallow_copy(other) 复制 header 但数据仍为借用,源对象同样须更长寿;需要独立缓冲区时改用 deep_copy()


⚖️ 6.12 容器与裸 Bytes 的对照

维度 Bytes zerocopy 容器
元数据 宽高 / 格式 / 形状 / 时间戳 / 类别等
反序列化拷贝 无(借用指针)
格式校验 有(check_valid
跨语言互操作 需自行约定协议 内置 Schema(PointCloud / Tensor
适用场景 通用小消息 传感器 / 模型 / 地图 / 检测 / 音频大负载

判据:小消息或已有自定义序列化时用 Bytes;传感器领域大负载优先用对应领域容器,并在 shm:// 上叠加传输层 loan 取得双层零拷贝。


🔗 相关文档

  • 通信模型 —— Publisher / Subscriber 收发用法与节点生命周期
  • 消息序列化 —— 序列化类型与零拷贝读写的关系
  • 传输后端与 URL —— shm / shm2 / zenoh 传输与 loan 配置
  • QoS 配置 —— 可靠性、历史深度与 profile
  • 安全加密 —— 序列化之后的认证加密管线,可与零拷贝叠加
  • 基础库 —— Bytes 类 API 与基础组件