julia调用c函数变量中指针的指针如何处理

我想调用的C函数长这样:

# * 获取指定时间段的指数历史Tick数据,接口支持单个代码或多个代码组合获取数据。
# *
# * @param symbol_list   指数代码列表,以逗号分开的市场.证券代码,如"sh.000001,sz.399992"
# * @param begin_time    开始时间,YYYY/MM/DD hh:mm:ss,如"2017/07/05 9:0:0"
# * @param end_time      结束时间,YYYY/MM/DD hh:mm:ss,如"2017/07/05 10:0:0"
# * @param itd           获取的指数tick数据
# * @param count         获取的数据个数
# * @return              成功返回0,失败返回错误码
int get_index_ticks(const char* symbol_list, const char* begin_time, 
                            const char* end_time, IndexTickData** itd, 
                            int* count);

其中IndexTickData是一个结构体:

#pragma pack(push, 1)
typedef struct t_IndexTickData{
  char symbol[12];   
  int32_t time;     
  int32_t open;     
  int32_t high;     
  int32_t low;  
  int32_t match; 
  int64_t volume;  
  int64_t turnover; 
  int32_t pre_close; 
} IndexTickData;
#pragma pack(pop)

这个函数中,整数型的count和指针的指针IndexTickData** 都是作为出参传递进去的,之所以使用指针的指针,是因为在调用之前没法事先预知会有多少条IndexTickData的数据,这个长度是不固定的。调用后会返回IndexTickData的数量,以及一个指针的指针IndexTickData**。通过这两个量就可以解析出全部的数据。我现在如果要在julia中调用这个函数,我能得到正确结果的方式如下:

using CBinding
c``
c""";
#pragma pack(push, 1)
typedef struct t_IndexTickData{
  char symbol[12];   
  int32_t time;     
  int32_t open;     
  int32_t high;     
  int32_t low;  
  int32_t match; 
  int64_t volume;  
  int64_t turnover; 
  int32_t pre_close; 
} IndexTickData;
#pragma pack(pop)
"""
symbol_list = "SZ.000001,SH.600000"
len = Ref{Cint}()
begin_time = "2021/09/16 9:0:0"
end_time = "2021/09/16 15:0:0"
#第一调用不知道定义多大的Vector合适,只能随便输入一个10
std =Vector{Cptr{c"SecurityTickData"}}(undef,10)
err = ccall((:get_index_ticks, lib), Int32, (Ptr{UInt8}, Ptr{UInt8}, Ptr{UInt8}, Cptr{Cptr{c"IndexTickData"}}, Ptr{Cint}), symbol_list, begin_time, end_time, std, len)
#调用后我通过len.x知道了数据条数,就可以根据它正确分配Vector的大小
std =Vector{Cptr{c"SecurityTickData"}}(undef,len.x)
err = ccall((:get_index_ticks, lib), Int32, (Ptr{UInt8}, Ptr{UInt8}, Ptr{UInt8}, Cptr{Cptr{c"IndexTickData"}}, Ptr{Cint}), symbol_list, begin_time, end_time, std, len)

这种方式有个问题,就是要调用两次,不够高效,不够优雅。有没有什么办法一次调用解决这个问题呢?

上面用vector的做法其实有些问题,正确的做法我试出来了,可以这样写:

using CBinding
c``
c""";
#pragma pack(push, 1)
typedef struct t_IndexTickData{
  char symbol[12];   
  int32_t time;     
  int32_t open;     
  int32_t high;     
  int32_t low;  
  int32_t match; 
  int64_t volume;  
  int64_t turnover; 
  int32_t pre_close; 
} IndexTickData;
#pragma pack(pop)
"""
symbol_list = "SZ.000001,SH.600000"
len = Ref{Cint}()
begin_time = "2021/09/16 9:0:0"
end_time = "2021/09/16 15:0:0"
std = Ref{Cptr{c"IndexTickData"}}(C_NULL)
err = ccall((:get_index_ticks, lib), Int32, (Ptr{UInt8}, Ptr{UInt8}, Ptr{UInt8}, Cptr{Cptr{c"IndexTickData"}}, Ptr{Cint}), symbol_list, begin_time, end_time, std, len)

在C里面Cptr{Cptr{c"IndexTickData"}}其实是一个指针数组,这里得到的std是这个指针数组第一个元素的引用,可以通过std来得到这个指针,用unsafe_load(std)可以得到这个指针指向的元素。这个指针数组第i个元素可以通过std+i-1来得到。因此,可以用如下方式解析出完整的数组。

res = c"IndexTickData"[]
for i = 1:len.x
     stdi = unsafe_load(std[] + i - 1)
     push!(res, stdi)
end

对于获取不定长的数组,一般可以使用两种方式。

第一种是调用方分配内存的方案,需要调用两次,第一次计算所需内存大小,第二次由被调用方写入内存。举一个例子,Win32有一个转换宽窄字符串的函数叫MultiByteToWideChar,你需要传递一个预分配的缓冲区进去,让它完成数据写入。最后一个参数cchWideChar表示你传给这个函数的缓冲区大小,如果为0那么函数不会操作缓冲区,而是返回需要的缓冲区大小,以便你申请堆内存后再次调用。

第二种是被调用方分配内存的方案,你通常可以直接拿到指针,这种的比较少见。

注意:

  1. 内存分配遵守谁分配谁释放的原则,所以优先使用第一种情况。如果使用了Vector的话Julia的GC会帮助你防止内存泄漏。Julia的ccall性能不低,不需要追求一次调用。
  2. 传入的长度参数必须和实际空间匹配,否则会造成缓冲区溢出,这是典型的安全问题来源。
  3. 你的Ref{Cint}()写法是有问题的,它不会初始化这片Cint内存空间,所以如果里面的值大于10(你的第一段代码)或者大于1(你的第二段代码),就会造成缓冲区溢出。如果你想将其初始化为0,应该使用Ref{Cint}(0)
  4. 解引用Ref对象请使用r[]而不是r.x
  5. IndexTickData**代表的是指针的数组,IndexTickData应该还是在堆上某处由库分配的,最好看一下这部分内存如何释放的问题,也许你用的这个库有自己的释放方法?一般这种情况下返回给你的是一个句柄,然后你使用句柄与库沟通,去获取具体的资源和释放内存。

谢谢azurefx的详细解答。这两种方式我都碰见了,第一种比较好处理,我基本上是调用两次,第一次获取这个数组的长度,第二次我就按这个长度分配Vector来调用。这个帖子里面描述的应该是你说的第二种,它会直接返回指针数组的头部,以及整个数组的长度。这个库里面没有释放机制,得自己释放。我想到的是拿到这个指针数组后,赶紧用unsafe_load解出来,然后存到一个 vector里面。这样做是不是后面就安全了?不会存在内存泄露问题?

另外还想请教一下,对于一个 Ref对象,len.x和len的区别是什么?如果我打印的话,似乎两者结果是一样的。

这个库里面没有释放机制,得自己释放。

如果是库分配的内存,只能由库释放,因为只有内存分配方才知道如何正确释放内存。举个例子,不同CRT的malloc实现不同,它内部有分配内存的数据结构,记录了内存长度等信息,如果混用实现很容易导致问题。

从这个get_index_ticks的函数签名上看,数组的内存是你自己分配的,但内部元素的内存块却是由库分配的。你把元素复制到Vector,是复制到了Julia管理的内存里,这部分数据是安全的,但数组里指针对应的IndexTickData内存块都要想办法释放,否则每次执行都会泄露内存。

它会直接返回指针数组的头部

那我认为这个接口的设计是有问题的,因为你无法获得一个安全的缓冲区大小,一旦它写入的元素个数超过容量,就会导致缓冲区溢出。std是一个Ref,等价于1个元素的Vector,你后续的元素其实都写进了不合法的内存里,造成不可预料的结果。正确的设计是参考我上面贴的MultiByteToWideChar的链接。

另外还想请教一下,对于一个 Ref对象,len.x和len的区别是什么?如果我打印的话,似乎两者结果是一样的。

len.xRef的内部实现,len[]Ref的对外接口。
你可以看看Ref的文档,如果它没有告诉你可以len.x,那就不要用。

我特意找到库的作者咨询了一下,发现我理解错了。其实这个C接口会自己去管理和释放内存。调用完成后,它自己会去清理,我调用完后及时把数据copy出来那是安全的。