跨语言调用是很方便实用的操作,但其实现并非想象的那么简单,包含有复杂的 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 比如这里用 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 里面的那些标注:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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/slides/Guinet-DragonFFI.pdf