我的C库生成了一个非常大的POD结构数组。将其传递给 Ruby 端的最有效方法是什么?在 Ruby 方面,原始值数组对我来说很好。
我当前的解决方案通过单独存储每个元素和字段来工作,并且速度非常慢。分析表明,此功能平均占用了大约 ~15% 的程序时间,甚至不是计算部分。
我已经阅读了Data_Wrap_Struct
,但不确定我需要它。如果我将原始void*
传递给字符串,然后在 Ruby 端解压缩它,它会更快吗?
struct SPacket
{
uint32_t field1;
uint32_t field2;
uint16_t field3;
uint8_t field4;
};
VALUE rb_GetAllData(VALUE self) // SLOOOW
{
size_t count = 0;
struct SPacket* packets = GetAllData(&count);
VALUE arr = rb_ary_new2(count);
for(size_t i = 0; i < count; i++)
{
VALUE sub_arr = rb_ary_new2(4);
rb_ary_store(sub_arr, 0, UINT2NUM(packets[i].field1));
rb_ary_store(sub_arr, 1, UINT2NUM(packets[i].field2));
rb_ary_store(sub_arr, 2, UINT2NUM(packets[i].field3));
rb_ary_store(sub_arr, 3, UINT2NUM(packets[i].field4));
rb_ary_store(arr, i, sub_arr);
}
return arr;
}
您的方法将 C 数组复制到 Ruby 数组中。你可以避免这种情况,我创建了一个 Ruby 集合类,该类使用 Data_Wrap_Struct
包装 C 数组并直接对其执行操作。
Data_Wrap_Struct
是一个宏,它采用一个 Ruby 类和一个 C struct
(以及可选的几个指向我故意省略的内存管理函数的指针(,并创建该类的实例,该实例具有"附加"struct
。在提供此类方法实现的函数中,您可以使用Data_Get_Struct
"解包"随后可以在函数中访问的struct
。
在这种情况下,如下所示:
// declare a variable for the new class
VALUE rb_cSPacketCollection;
// a struct that will be wrapped by the class
struct SPacketDataStruct {
struct SPacket * data;
int count;
};
VALUE rb_GetAllData() {
struct SPacketDataStruct* wrapper = malloc(sizeof (struct SPacketCollectionWrapper));
wrapper->data = GetAllData(&wrapper->count);
return Data_Wrap_Struct(rb_cSPacketCollection, 0, 0, wrapper);
}
在Init_whatever()
方法中,您需要创建类:
rb_cSPacketCollection = rb_define_class("SPacketCollection", rb_cObject);
仅此一项用处不大,您需要在这个新类上定义一些方法。例如,您可以创建一个[]
方法来允许访问各个SPacket
:
VALUE SPacketCollection_get(VALUE self, VALUE index) {
// unwrap the struct
struct SPacketDataStruct* wrapper;
Data_Get_Struct(self, struct SPacketDataStruct, wrapper);
int i = NUM2INT(index);
// bounds check
if (i >= wrapper->count) {
rb_raise(rb_eIndexError, "Index out of bounds");
}
// just return an array in this example
VALUE arr = rb_ary_new2(4);
rb_ary_store(arr, 0, UINT2NUM(wrapper->data[i].field1));
rb_ary_store(arr, 1, UINT2NUM(wrapper->data[i].field2));
rb_ary_store(arr, 2, UINT2NUM(wrapper->data[i].field3));
rb_ary_store(arr, 3, UINT2NUM(wrapper->data[i].field4));
return arr;
}
然后在Init_
方法中,在创建类后定义方法:
rb_define_method(rb_cSPacketCollection, "[]", SPacketCollection_get, 1);
请注意Data_Get_Struct
是一个宏,用法有点奇怪,因为它不返回未包装的struct
。
由于您在此阶段已经开始使用 Data_Wrap_Struct
,因此您可以更进一步创建一个新类,该类包装单个 SPacket
结构并直接对其进行操作:
// declare a variable for the new class
VALUE rb_cSPacket;
//and a function to get a field value
// you'll need to create more methods to access the other fields
// (and possibly to set them)
VALUE SPacket_field1(VALUE self) {
struct SPacket* packet;
Data_Get_Struct(self, struct SPacket, packet);
return UINT2NUM(packet->field1);
}
在Init_
函数中,创建它并定义方法:
rb_cSPacket = rb_define_class("SPacket", rb_cObject);
rb_define_method(rb_cSPacket, "field1", SPacket_field1, 0);
这可能需要一些工作来为田地创建所有 getter 和 setter,这取决于您如何使用它。像 ffi 这样的东西可以在这里提供帮助,但我不知道 ffi 将如何处理集合类——它可能值得研究。
现在更改您的 []
函数以返回一个实例,如果这个新类:
VALUE SPacketCollection_get(VALUE self, VALUE index) {
//unwrap the struct
struct SPacketDataStruct* wrapper;
Data_Get_Struct(self, struct SPacketDataStruct, wrapper);
int i = NUM2INT(index);
//bounds check
if (i >= wrapper->count) {
rb_raise(rb_eIndexError, "Index out of bounds");
}
//create an instance of the new class, and wrap it around the
//struct in the array
struct SPacket* packet = &wrapper->data[i];
return Data_Wrap_Struct(rb_cSPacket, 0, 0, packet);
}
有了这个,你现在可以在 Ruby 中做这样的事情:
c = get_all_data # in my testing I just made this a global method
c[2].field1 # returns the value of field1 of the third SPacket in the array
可能值得在集合类上创建一个 each
方法,然后您可以包含 Enumerable
模块并提供大量方法:
VALUE SPacketCollection_each(VALUE self) {
//unwrap the struct as before
struct SPacketDataStruct* wrapper;
Data_Get_Struct(self, struct SPacketDataStruct, wrapper);
int i;
for(i = 0; i < wrapper->count; i++) {
//create a new instance if the SPacket class
// wrapping this entry
struct SPacket* packet = &wrapper->data[i];
rb_yield(Data_Wrap_Struct(rb_cSPacket, 0, 0, packet));
}
return self;
}
在Init_whatever
:
rb_define_method(rb_cSPacketCollection, "each", SPacketCollection_each, 0);
rb_include_module(rb_cSPacketCollection, rb_mEnumerable);
在这个例子中,我没有关心对象标识和内存管理之类的事情。由于所有内容都由同一数组支持,因此您可能有多个对象共享相同的数据,您必须考虑这是否适合您使用。你可能已经注意到我已经malloc
了,但没有free
d。您需要确定谁"拥有"数据数组,并确保不会引入任何内存泄漏。您可以将函数传递给Data_Wrap_Struct
,该函数将在对象被垃圾回收以释放内存时调用。
如果你还没有看过,Pickaxe 书中有一章关于 C 扩展的好章节,现在可以在线获得。