Clang.jl 的使用交流


#1

最近需要对好几个C/C++的库做封装,感觉趟了好多坑…

之前只用过 CxxWrap.jl,我自己的体验是,如果库本身自带了Python的binding,用 CxxWrap.jl 还算行(<=Python的工作量)。昨天需要对 deepmind/hanabi-learning-environment 这个库做封装,初看了下代码,感觉比较适合用 Clang.jl。于是乎:

1. 先用BinaryBuilder.jl 打包

  • 本地Ubuntu测试的时候,一开始没有加sudo (我清楚记得以前会提示的,后来不知道为什么没有提示了),一直出错。加上之后,本地编译正常了。
    • 原始的代码库 deepmind/hanabi-learning-environmentCMakeFiles.txt 里,没有 install 的步骤,手动fork之后自己添加了相应的install,这样 BinaryBuilder 运行后才会在 products 目录中有编译结果 (需要的话把一些.h文件也打进去)
    • 这个库本身没有太多依赖,相应的builder比较简单

Q1: 我指定 platforms = supported_platforms() 之后,travis的编译会提示MacOS没有授权之类的,会导致出错,那 @Gnimuc 写的 NuklearBuilder.jl 这种包含 MacOS 的release怎么生成的?

2. 创建Julia Package

然后是创建 Hanabi.jl,重命名上面生成的 build_XXX.jl文件为 deps/build.jl, 用于安装包的时候生成deps.jl文件。然后创建 gen 目录,用Clang.jl生成相应的wrapper:

参照 Clang.jl提供的例子执行之后,会得到一个LibTemplate.jl的文件,这个模板需要稍加修改,目前都是针对单个header文件写死了的。

Q2:执行过程中会报错:不知道有没有影响?

/home/tj/data/workspace/github/Hanabi/deps/usr/include/pyhanabi.h:91:1: error: unknown type name 'bool'
/home/tj/data/workspace/github/Hanabi/deps/usr/include/pyhanabi.h:92:1: error: unknown type name 'bool'
/home/tj/data/workspace/github/Hanabi/deps/usr/include/pyhanabi.h:93:1: error: unknown type name 'bool'
/home/tj/data/workspace/github/Hanabi/deps/usr/include/pyhanabi.h:94:1: error: unknown type name 'bool'
/home/tj/data/workspace/github/Hanabi/deps/usr/include/pyhanabi.h:131:1: error: unknown type name 'bool'
/home/tj/data/workspace/github/Hanabi/deps/usr/include/pyhanabi.h:132:1: error: unknown type name 'bool'
/home/tj/data/workspace/github/Hanabi/deps/usr/include/pyhanabi.h:180:1: error: unknown type name 'bool'

Q3: 原始的 pyhanabi.h 是包含 extern C {} 语句的,我手动去掉了之后调用的Clang,不去掉的话就会报错

/home/tj/data/workspace/github/Hanabi/deps/usr/include/pyhanabi.h:24:8: error: expected identifier or '('
似乎是parser的原因?

Q4: 一般什么时候用 Clang.jl Cxx.jl CxxWrap.jl 呢?

我现在还需要封装另外两个库:

不确定应该采用哪种方式比较合适,不知道有没有什么通用的建议?

Q5: 如何理解Ptr{T}Ref{T}

道理我都懂,文档里也写了(PS: 这部分中文文档似乎缺失了)
https://docs.julialang.org/en/v1/manual/calling-c-and-fortran-code/#When-to-use-T,-Ptr{T}-and-Ref{T}-1

In Julia code wrapping calls to external C routines, ordinary (non-pointer) data should be declared to be of type T inside the ccall , as they are passed by value. For C code accepting pointers, Ref{T} should generally be used for the types of input arguments, allowing the use of pointers to memory managed by either Julia or C through the implicit call to Base.cconvert . In contrast, pointers returned by the C function called should be declared to be of output type Ptr{T} , reflecting that the memory pointed to is managed by C only. Pointers contained in C structs should be represented as fields of type Ptr{T} within the corresponding Julia struct types designed to mimic the internal structure of corresponding C structs.

In Julia code wrapping calls to external Fortran routines, all input arguments should be declared as of type Ref{T} , as Fortran passes all variables by pointers to memory locations. The return type should either be Cvoid for Fortran subroutines, or a T for Fortran functions returning the type T .

上面这段话的大意是说, Ref{T}类型是Julia分配的,用于GC的管理,而Ptr{T}是C管理的,在:ccall的时候前者会自动转换。但我不太确定下面的这种写法是正确的:

比如,下面这段通过Clang自动生成的libhanabi_common.jl中的一部分:

我在Julia中构造该结构体的时候,目前是这样写的:

game = Ref{Ptr{Cvoid}}()
g = PyHanabiGame(game[])

然后为了调用下面的接口函数:

我需要再把上面的封装起来:

my_game = Ref(g)
n = NumPlayers(my_game[])

才能得到正常的结果。 上面这种写法参考的以下内容:

但是我不确定这样子是否合理,因为中间还生成了好几个Ref


#2

需要将travis.yml里的BINARYBUILDER_AUTOMATIC_APPLE改为true:

env:
  global:
    - BINARYBUILDER_DOWNLOADS_CACHE=downloads
    - BINARYBUILDER_AUTOMATIC_APPLE=true

bool是非标准类型,需要#include <stddef.h>

extern C应该没问题,有可能是别的地方的bug, 我晚上看一下。

if has C API
    if large API base
        use Clang.jl
    else
        manually write `ccall`
    end
else
    use CxxWrap.jl
end

update: 一般C API都会直接操作handle(pointer), API用起来通常不会频繁的解引用。这里的struct PyHanabiGame game::Ptr{Cvoid} end定义的关键是和C side的结构体内存结构一致,所以要用Ptr, 这也是文档提到的:“ Pointers contained in C structs should be represented as fields of type Ptr{T} within the corresponding Julia struct types designed to mimic the internal structure of corresponding C structs.”

根据NumPlayers的 signature: Ptr{pyhanabi_game_t}, 一般直接把Ptr换成Ref定义就可以了:

my_game = Ref{PyHanabiGame}()
n = NumPlayers(my_game)

#3

额,我只能说这里文档写的也并不是很清晰(有可能这部分内容本身就很难写清楚),所以挺难总结出一个简单且非具体的原则,需要具体情况具体分析。我写一下我的理解:

1. 首先讨论的前提是在发生 interop 的条件下,在不发生的情况下,RefPtr 都可以是由Julia分配的并且都可以被GC回收。(e.g. A= [1]; pointer(A) #->Ptr{Int64} @0x00000000099cdcd0)
2. Ref{T} 类型是Julia分配的,在1.的条件下,它经常作为一个容器传递给C,它的内容(Ptr)会在C side被改写:

x = Ref{Foo}()
ccall((:xxx, yyy), Cvoid, (Ptr{Foo},), x)
x[]
3. 从ccall发生开始,到返回之前,Ref可以确保其内容不被GC回收,即可以保证在C side可以安全的完成改写,但`Ptr` 却不能,需要用`GC.@preserve`来暂时关闭GC,直到ccall返回: ```julia outptr = ccall((:zzz, www), Ptr{Bar}, (Cint, ), 1) GC.@preserve outptr begin # outptr是一个Ptr,虽然它指向的内存不是JuliaGC负责管理的,但这个Ptr本身有可能是在栈上,甚至有可能会被编译期优化掉,进而导致下面的ccall gc error v = ccall((:iii, ppp), Cint, (Ptr{Bar},), outptr) end # use v hereafter ``` 2. 和 3. 是在Julia-C interop的常见两种cases. [quote="Jun, post:1, topic:1618"] In Julia code wrapping calls to external Fortran routines, all input arguments should be declared as of type `Ref{T}` , as Fortran passes all variables by pointers to memory locations. The return type should either be `Cvoid` for Fortran subroutines, or a `T` for Fortran functions returning the type `T` . [/quote] 再多说一下`Ref`的问题,这里文档说明了是在调Fortran的前提下, 这是因为Fortran比较特殊,它的函数传递“都必须”(严格地说有例外,但不影响之后要讲的东西)是引用,比如在传递一个值`x = Cint(1)`给Fortran的subroutine时, 如果Fortran不会更改这个值的内容,那么直接传递`x`就可以,相应的ccall signature要写成`Ref{Cint}`,不能是`Cint`,注意这里不需要显式地包一层`Ref(x)`再进行传递,因为Julia会隐式做转换(Base.cconvert)。 这也就意味着,没有必要特别地将一些 struct 地 field 声明成 `Ref`, which 可能会导致代码中冗余地解引用`[]`, 比如[这个例子](https://github.com/Gnimuc/LBFGSB.jl/blob/master/src/wrapper.jl)。

#4

非常感谢!!!

那这里我明白了为什么许多代码里都要写这个了。

确实,这段话一开始没看懂。。。


#5

它原来的代码不太稳健,应该这样写来保证C/C++都可以编译过:

#ifdef __cplusplus
extern "C" {
#endif

#ifdef __cplusplus
}
#endif

#6

事实上是有的: https://github.com/JuliaInterop/Clang.jl/issues/120#issuecomment-478334164