iceoryx2 动态类型:Slice 动态 payload 与多段扩容
iceoryx2 的 publish-subscribe 模式默认使用固定大小的 payload 类型,消息长度在编译期确定。当消息大小无法提前知道(传感器帧、变长字节流)时,就需要动态类型——C++ 侧的 bb::Slice<T>、Rust 侧的 [T]。本文覆盖动态类型的 C++/Rust 用法、AllocationStrategy 分配策略,以及多段共享内存扩容、PointerOffset 编码等底层机制,适合已经在用 iceoryx2、需要发送变长消息的开发者。
固定类型 vs 动态类型
| 方面 | 固定类型 (FixedSize) | 动态类型 (Dynamic) |
|---|---|---|
| C++ Payload | MyStruct, uint64_t | bb::Slice<uint8_t> |
| Rust Payload | MyStruct, u64 | [u8] |
| 内部标记 | TypeVariant::FixedSize | TypeVariant::Dynamic |
| 内存段 | 静态 SharedMemory<PoolAllocator> | ResizableSharedMemory(可多段扩容) |
| loan API | loan_uninit() → 单个元素 | loan_slice_uninit(n) → n 个元素 |
| 每条消息大小 | 永远 sizeof(T) | 每次 loan 可不同 |
| 内存增长 | 不增长 | 按 AllocationStrategy 增长 |
| Publisher 额外配置 | 无 | initial_max_slice_len + allocation_strategy |
| Subscriber 额外配置 | 无 | 无(透明接收) |
核心思想:Publisher 的共享内存数据段可以在运行时自动扩容,创建新的更大的共享内存段来满足更大的 slice 请求。
C++ 用法
Publisher 端
#include "iox2/iceoryx2.hpp"
constexpr iox2::bb::Duration CYCLE_TIME = iox2::bb::Duration::from_secs(1);
auto main() -> int {
using namespace iox2;
auto node = NodeBuilder().create<ServiceType::Ipc>().value();
// 步骤1: 声明动态 payload 类型 bb::Slice<T>
auto service = node.service_builder(ServiceName::create("My/Dynamic/Service").value())
.publish_subscribe<bb::Slice<uint8_t>>()
.open_or_create()
.value();
// 步骤2: 配置 Publisher 的初始大小和增长策略
auto publisher = service.publisher_builder()
.initial_max_slice_len(16) // 初始最多 16 个元素
.allocation_strategy(AllocationStrategy::PowerOfTwo) // 不够时按 2 的幂扩容
.create()
.value();
uint64_t counter = 0;
while (node.wait(CYCLE_TIME).has_value()) {
// 步骤3: 每次请求不同大小的 slice
uint64_t required_size = (counter + 1) * (counter + 1);
auto sample = publisher.loan_slice_uninit(required_size).value();
// 步骤4: 初始化数据
auto initialized = sample.write_from_fn(
[&](uint64_t idx) { return static_cast<uint8_t>((idx + counter) % 255); });
// 步骤5: 发送
send(std::move(initialized)).value();
counter++;
}
} Subscriber 端
Subscriber 端无需特殊配置,payload 自动解析为 ImmutableSlice:
#include "iox2/iceoryx2.hpp"
auto main() -> int {
using namespace iox2;
auto node = NodeBuilder().create<ServiceType::Ipc>().value();
auto service = node.service_builder(ServiceName::create("My/Dynamic/Service").value())
.publish_subscribe<bb::Slice<uint8_t>>()
.open_or_create()
.value();
auto subscriber = service.subscriber_builder().create().value();
while (node.wait(iox2::bb::Duration::from_secs(1)).has_value()) {
auto sample = subscriber.receive().value();
while (sample.has_value()) {
auto payload = sample->payload(); // bb::ImmutableSlice<const uint8_t>
// 查询长度
auto num_elements = payload.number_of_elements();
auto num_bytes = payload.number_of_bytes();
// 按索引访问
auto first = payload[0];
// range-for 遍历
for (const auto& byte : payload) {
// ...
}
sample = subscriber.receive().value();
}
}
} 初始化 API 汇总
SampleMutUninit 在动态类型下提供三种初始化方式:
// 方式1: write_from_fn -- 用 lambda 按索引初始化
auto sample = publisher.loan_slice_uninit(100).value();
auto initialized = sample.write_from_fn([](uint64_t i) {
return static_cast<uint8_t>(i % 255);
});
send(std::move(initialized)).value();
// 方式2: write_from_slice -- 从已有 slice 拷贝
uint8_t data[] = {1, 2, 3, 4, 5};
bb::ImmutableSlice<uint8_t> source(data, 5);
auto sample2 = publisher.loan_slice_uninit(5).value();
auto initialized2 = sample2.write_from_slice(source);
send(std::move(initialized2)).value();
// 方式3: 手动 placement new + assume_init
auto sample3 = publisher.loan_slice_uninit(10).value();
auto slice = sample3.payload_mut(); // bb::MutableSlice<uint8_t>
for (uint64_t i = 0; i < slice.number_of_elements(); ++i) {
new (&slice[i]) uint8_t(42);
}
auto initialized3 = assume_init(std::move(sample3));
send(std::move(initialized3)).value(); Publisher 还提供了零拷贝和拷贝两种发送风格:
// 零拷贝: loan -> init -> send (推荐)
auto sample = publisher.loan_slice_uninit(n).value();
// ... 初始化 ...
send(std::move(initialized)).value();
// 拷贝: send_slice_copy (简单但多一次 memcpy)
uint8_t data[] = {1, 2, 3};
bb::ImmutableSlice<uint8_t> slice(data, 3);
publisher.send_slice_copy(slice).value(); Rust 用法
use iceoryx2::prelude::*;
let node = NodeBuilder::new().create::<ipc::Service>()?;
// payload 类型为 [u8] (Rust 的 unsized slice type)
let service = node
.service_builder(&"My/Dynamic/Service".try_into()?)
.publish_subscribe::<[u8]>()
.open_or_create()?;
let publisher = service
.publisher_builder()
.initial_max_slice_len(16)
.allocation_strategy(AllocationStrategy::PowerOfTwo)
.create()?;
let required_size = 100;
let sample = publisher.loan_slice_uninit(required_size)?;
let sample = sample.write_from_fn(|idx| (idx % 255) as u8);
sample.send()?; AllocationStrategy 分配策略
enum class AllocationStrategy : uint8_t {
BestFit, // 精确匹配,节省内存,重分配频繁
PowerOfTwo, // 向上取 2 的幂,减少重分配,可能浪费内存
Static // 不增长,超出 initial_max_slice_len 则返回 OutOfMemory
}; 增长行为示例(initial_max_slice_len = 16):
请求长度: 16 → 20 → 50 → 100
BestFit: 16 → 20 → 50 → 100 每次新建精确大小的 segment
PowerOfTwo: 16 → 32 → 64 → 128 每次新建 2^n 大小的 segment
Static: 16 → ERR → ERR → ERR 超过 16 就返回 LoanError::ExceedsMaxLoanSize 选择建议:
| 场景 | 推荐策略 |
|---|---|
| 消息大小变化剧烈(如传感器帧) | PowerOfTwo |
| 消息大小变化很小 | BestFit |
| 消息大小固定(已知最大值) | Static |
| 对延迟极敏感(避免运行时扩容) | Static + 设足够大的 initial_max_slice_len |
底层原理
TypeVariant 标记
Rust 核心定义(iceoryx2/src/service/static_config/message_type_details.rs):
#[repr(C)]
pub enum TypeVariant {
FixedSize, // 编译期固定大小
Dynamic, // 运行时动态大小 (slice)
} C FFI 映射(iceoryx2-ffi/c/src/api/service_builder_pub_sub.rs):
#[repr(C)]
pub enum iox2_type_variant_e {
FIXED_SIZE,
DYNAMIC,
} C++ 自动推断(iceoryx2-cxx/include/iox2/service_builder_publish_subscribe.hpp):
auto type_variant = bb::IsSlice<Payload>::VALUE
? iox2_type_variant_e_DYNAMIC
: iox2_type_variant_e_FIXED_SIZE; bb::IsSlice<T> 是编译期 trait:对 bb::Slice<T> 返回 true,其他类型返回 false。用户无需手动设置,模板会自动推断。
类型信息的传递链路
C++ 模板参数 bb::Slice<uint8_t>
│
▼ bb::IsSlice<Payload>::VALUE → true
ServiceBuilder::set_parameters()
│ type_variant = iox2_type_variant_e_DYNAMIC
│ type_name = "u8", size = 1, alignment = 1
▼
iox2_service_builder_pub_sub_set_payload_type_details() ← C FFI 函数
│
▼ TypeVariant::Dynamic
Rust ServiceBuilder → MessageTypeDetails { payload: TypeDetail { variant: Dynamic, ... } }
│
▼ 写入 Service 的 StaticConfig(持久化到文件/共享内存)
其他进程 open 时读取 StaticConfig 并检查兼容性 MessageTypeDetails 与内存布局
每条消息在共享内存中的布局:
┌────────────────┬─────────────────┬───────────────────────────────┐
│ Header │ UserHeader │ Payload (动态) │
│ (FixedSize) │ (FixedSize) │ T[0] T[1] T[2] ... T[n-1] │
│ sizeof(Header)│ sizeof(UH) │ n * sizeof(T) │
│ align(Header) │ align(UH) │ align(T) │
└────────────────┴─────────────────┴───────────────────────────────┘ sample 总大小的计算公式(message_type_details.rs):
fn sample_layout(&self, number_of_elements: usize) -> Layout {
Layout::from_size_align_unchecked(
align(
header.size + user_header.size + user_header.alignment - 1
+ payload.size * number_of_elements // <-- 动态部分
+ payload.alignment - 1,
header.alignment,
),
header.alignment,
)
} FixedSize时number_of_elements = 1,布局创建时就确定Dynamic时number_of_elements由每次loan_slice_uninit(n)的参数决定
ResizableSharedMemory 多段扩容
固定类型使用单个 SharedMemory<PoolAllocator>,所有 chunk 大小一样。动态类型使用 ResizableSharedMemory,可以创建多个 segment:
loan_slice_uninit(16): → 从 Segment 0 分配 (chunk_size = 16*sizeof(T))
loan_slice_uninit(20): → 16 不够!创建 Segment 1 (chunk_size = 32*sizeof(T), PowerOfTwo)
loan_slice_uninit(50): → 32 不够!创建 Segment 2 (chunk_size = 64*sizeof(T))
loan_slice_uninit(30): → Segment 1 的 32 够用,从 Segment 1 分配
Segment 0 (SegmentId=0) Segment 1 (SegmentId=1) Segment 2 (SegmentId=2)
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Pool: 16 * sizeof(T)│ │ Pool: 32 * sizeof(T)│ │ Pool: 64 * sizeof(T)│
│ [chunk][chunk][...] │ │ [chunk][chunk][...] │ │ [chunk][chunk][...] │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘ 每个 segment 是独立的 POSIX 共享内存对象。旧 segment 不会被删除(已发出的 sample 可能仍在被 subscriber 使用)。
PointerOffset 编码
跨进程传递的不是指针,而是 PointerOffset(一个 u64):
PointerOffset (u64):
┌─────────────────────────────────┬──────────────┐
│ offset (高 56 位) │ SegmentId │
│ segment payload 区域内的偏移 │ (低 8 位) │
└─────────────────────────────────┴──────────────┘ SegmentId标识是哪个 segment(0~255)offset是 segment 内 payload 区域的字节偏移
还原为本进程指针:地址 = payload_start_address(segment_id) + offset
SegmentId 只有 8 位,意味着最多 256 个 segment。使用 PowerOfTwo 策略能有效控制 segment 数量(每次翻倍,从 16 到 2^31 只需约 16 个 segment)。
Subscriber 延迟映射
Subscriber 在收到一个指向新 segment 的 PointerOffset 时,会延迟打开并映射该 segment:
Subscriber 收到 PointerOffset(offset=100, segment_id=2)
│
├─ segment_id=2 已映射? → 是 → 直接计算地址
│
└─ 未映射? → 打开共享内存 segment 2 → mmap → 缓存映射 → 计算地址 服务兼容性检查
当进程 open 一个已存在的 service 时,TypeVariant 参与兼容性检查:
fn is_compatible_to(&self, rhs: &Self) -> bool {
// ...
&& self.payload.variant == rhs.payload.variant // variant 必须一致
&& self.payload.type_name == rhs.payload.type_name
&& self.payload.size == rhs.payload.size
&& self.payload.alignment <= rhs.payload.alignment
} 不兼容的情况(会返回 IncompatibleTypes 错误):
// 进程 A: 创建 FixedSize 服务
node.service_builder(name).publish_subscribe<uint64_t>().create();
// 进程 B: 尝试用 Dynamic 类型打开 → 失败!
node.service_builder(name).publish_subscribe<bb::Slice<uint64_t>>().open(); // ERROR 兼容的情况:
// 进程 A 和 B 都使用同一种类型
// 两边都是 bb::Slice<uint8_t> → OK
// 两边都是 MyStruct → OK(且 type_name, size, alignment 必须匹配) Payload 类型约束
bb::Slice<T> 中的 T 必须满足:
| 要求 | 说明 |
|---|---|
std::is_trivially_copyable | 可安全 memcpy |
| 无堆内存 | 不能含 std::string, std::vector |
| 无指针成员 | 指针跨进程无效 |
| 无虚函数 | vtable 指针跨进程无效 |
std::is_trivially_destructible | 析构不能有副作用 |
#[repr(C)] / 确定内存布局 | 跨编译单元布局一致 |
安全的元素类型:
bb::Slice<uint8_t> // 字节流
bb::Slice<float> // 浮点数组
bb::Slice<int32_t> // 整数数组
bb::Slice<MyPodStruct> // 自定义 POD struct (无指针、无虚函数) 不安全的元素类型:
bb::Slice<std::string> // 堆内存!
bb::Slice<MyClassWithPtr> // 含指针! 实践建议
initial_max_slice_len尽量设准。如果 90% 的消息在 1KB 以内,设1024(对uint8_t),减少运行时扩容。- 选对
AllocationStrategy:变化剧烈选PowerOfTwo(用内存换少扩容);变化很小选BestFit(精确但多扩容);大小已知选Static(零扩容,最高性能)。 - 注意 SegmentId 上限。最多 256 个 segment,
BestFit策略下频繁不同大小的请求可能快速耗尽;PowerOfTwo的指数增长能有效控制。 - Subscriber 无需关心动态细节,只管
receive(),payload 长度信息在number_of_elements()中。 - 避免
SampleMut跨线程。SampleMut析构时操作 Publisher 的内部状态,Publisher不是线程安全的。 - 及时释放
Sample。Subscriber 长时间持有Sample会阻止 chunk 回收,可能导致 PublisherOutOfMemory。
关键源码索引
路径均相对 iceoryx2 仓库根目录:
| 主题 | 文件路径 |
|---|---|
TypeVariant / TypeDetail 定义 | iceoryx2/src/service/static_config/message_type_details.rs |
C FFI iox2_type_variant_e 定义 | iceoryx2-ffi/c/src/api/service_builder_pub_sub.rs |
| C++ 自动推断 TypeVariant | iceoryx2-cxx/include/iox2/service_builder_publish_subscribe.hpp → set_parameters() |
bb::Slice<T> 定义 | iceoryx2-bb/cxx/include/iox2/bb/slice.hpp |
bb::IsSlice<T> trait | iceoryx2-bb/cxx/include/iox2/bb/slice.hpp (底部) |
AllocationStrategy 定义 | iceoryx2-cxx/include/iox2/allocation_strategy.hpp |
PortFactoryPublisher (C++ builder) | iceoryx2-cxx/include/iox2/port_factory_publisher.hpp |
Publisher loan/send 实现 (C++) | iceoryx2-cxx/include/iox2/publisher.hpp |
SampleMutUninit 初始化 API (C++) | iceoryx2-cxx/include/iox2/sample_mut_uninit.hpp |
SampleMut send/drop (C++) | iceoryx2-cxx/include/iox2/sample_mut.hpp |
| Rust Publisher 核心实现 | iceoryx2/src/port/publisher.rs |
| Rust SampleMut 生命周期 | iceoryx2/src/sample_mut.rs |
PointerOffset 编码 | iceoryx2-cal/src/shm_allocator/pointer_offset.rs |
SegmentState 引用计数 | iceoryx2/src/port/details/segment_state.rs |
DataSegment 数据段管理 | iceoryx2/src/port/details/data_segment.rs |
| ResizableSharedMemory | iceoryx2-cal/src/resizable_shared_memory/ |
| C++ 动态数据示例 (Publisher) | examples/cxx/publish_subscribe_dynamic_data/src/publisher.cpp |
| C++ 动态数据示例 (Subscriber) | examples/cxx/publish_subscribe_dynamic_data/src/subscriber.cpp |
| Rust 动态数据示例 (Publisher) | examples/rust/publish_subscribe_dynamic_data/publisher.rs |