跨语言调用是很方便实用的操作,但其实现并非想象的那么简单,包含有复杂的 ABI 设计、语言间的兼容交互等,本文将介绍一个跨语言调 C 库:DragonFFI。
1、定义
在官方 github 仓库中就有相关的说明,DragonFFI 是 C 语言的 FFI(Foreign Function Interface,外部函数接口)库,使用 C++ 编写,基于 clang/llvm 来实现。
跨语言调用 C ,一般有通过手写胶水代码的(JNI,Python,Ruby)、生成胶水代码的(SWIG)、扩展 C 的(C++,Objective-C)。
而 FFI 即其他语言可以通过它所提供的 API 和绑定来调用 C 语言的函数。
A foreign function interface (FFI) is a mechanism by which aprogram written in one programming language can call routines ormake use of services written in another.
# 比如这里用 python 进行 C 语言函数的调用
import pydffi
CU = pydffi.FFI().cdef("int puts(const char* s);");
CU.funcs.puts("hello world!")
# JIT
# First, declare an FFI context
F = pydffi.FFI()
# Then, compile a module and get a compilation unit
CU = F.compile("int add(int a, int b) { return a+b; }")
# And call the function
print(int(CU.funcs.add(4, 5)))
2、与其他 FFI 库的对比
libffi 是有名的提供 C 语言 FFI 库。但它不支持最近的一些调用约定(calling convention),比如 Linux x64 系统的 Microsoft x64 ABI(Application Binary Interface),而且其每个 ABI 还需要手写汇编,其 ABI 也变得复杂,尤其是在传递结构体类型时。
cffi 是 libffi 当中所提供的 python 绑定,它还使用了 pycparser 来声明接口和类型。但它所使用的 C 语言的解析器不支持 include 和一些函数的 attribute,因此需要自行维护调用 C 库的头文件。
DragonFFI 基于 clang/llvm 实现,得以使用 clang 解析头文件而不用自行适配、可以动态编译(支持 JIT)、支持很多调用约定和函数的 attribute。
DragonFFI 目前支持 Linux、OSX 和 Windows,支持 Intel 32 位和 64 位 CPU。
3、如何解析 C 语言的类型
为了解析 C 语言的类型,我们需要以下的一些基本信息:
- 函数类型,以及调用约定
- 结构体:偏移量、名字
- union/enum:域名(值)
一方面,我们看到 LLVM IR 过于底层(low level),十分复杂;另一方面,Clang AST 又过于抽象(high level),没有一些类型信息(比如结构体布局对齐等)。
比较合适的方式是,使用 LLVM metadata (元数据,描述数据的数据),由 Clang 生成 DWARF(debugging with attributed record formats) 或者 PDB(Program Database) 结构体时产生。它们包含有基本的类型描述信息、函数调用约定。
注:PDB 是一种文件格式,和 DWARF 是一个概念,由微软开发出来,包含有调试信息。
这些 LLVM metadata 的具体例子,就是嵌在 IR 里面的那些标注:
target triple = "x86_64-pc-linux-gnu"
%struct.A = type { i16, i32 }
@.str = private unnamed_addr constant [7 x i8] c"%d %d\0A\00", align 1
define void @print_A(i64) local_unnamed_addr !dbg !7 {
%2 = trunc i64 %0 to i32
%3 = lshr i64 %0, 32
%4 = trunc i64 %3 to i32
tail call void @llvm.dbg.value(metadata i32 %4, i64 0, metadata !18, metadata !19), !dbg !20
tail call void @llvm.dbg.declare(metadata %struct.A* undef, metadata !18, metadata !21), !dbg !20
%5 = shl i32 %2, 16, !dbg !22
%6 = ashr exact i32 %5, 16, !dbg !22
%7 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([...] @.str, i64 0, i64 0), i32 %6, i32 %4), !dbg !23
ret void, !dbg !24
}
[...]
; DISubprogram defines (in our case) a C function, with its full type
!7 = distinct !DISubprogram(name: "print_A", scope: !1, file: !1, line: 6, type: !8, [...], variables: !17)
; This defines the type of our subprogram
!8 = !DISubroutineType(types: !9)
; We have the "original" types used for print_A, with the first one being the
; return type (null => void), and the other ones the arguments (in !10)
!9 = !{null, !10}
!10 = !DIDerivedType(tag: DW_TAG_typedef, name: "A", file: !1, line: 4, baseType: !11)
; This defines our structure, with its various fields
!11 = distinct !DICompositeType(tag: DW_TAG_structure_type, file: !1, line: 1, size: 64, elements: !12)
!12 = !{!13, !15}
; We have here the size and name of the member "a". Offset is 0 (default value)
!13 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !11, file: !1, line: 2, baseType: !14, size: 16)
!14 = !DIBasicType(name: "short", size: 16, encoding: DW_ATE_signed)
; We have here the size, offset and name of the member "b"
!15 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !11, file: !1, line: 3, baseType: !16, size: 32, offset: 32)
!16 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
[...]
4、实现
DragonFFI 首先会解析 LLVM IR 里所包含的调试信息,然后创建一个自定义的类型系统,来表示各种各样的函数类型、结构体、枚举、typedef。
之所以创建一个自定义的类型系统,一是因为只需要从元数据树(metadata tree)中获取所需信息就够了(不用所有的调试信息),二是因为可以让 DragonFFI 的公开头文件不需要依赖 LLVM 的头文件。
依赖于 Clang,减少了大量的工作(函数重定义、类型错误、函数对外不可见、类型的内存布局等)。
参考
DragonFFI:https://github.com/aguinet/dragonffi
libffi:https://sourceware.org/libffi/
cffi:https://cffi.readthedocs.io/en/latest/
DWARF:http://dwarfstd.org/
PDB:https://llvm.org/docs/PDB/index.html
DragonFFI: FFI/JIT for the C language using Clang/LLVM:https://blog.llvm.org/2018/03/dragonffi-ffijit-for-c-language-using.html
Skip the FFI!:https://llvm.org/devmtg/2014-10/Slides/Skip%20the%20FFI.pdf
Guinet-DragonFFI:https://llvm.org/devmtg/2018-04