Racket FFI 3

:: Racket, FFI

这是关于Racket的FFI的使用的第三篇讲解.在这篇博文中我们会涉及到一些底层的操作.包括指针,联合体和自定义的C数据类型.主要还是自定义类型,它可以让你在使用racket和c互交的时候进行抽象进而不需要了解具体的c语言的表现形式.

准备工作

正如我们在第二篇博文中所做的那样,我们会从一些已有的代码出发,在此基础上完成我们的既定目标.但是首先,每次都需要写#:c-id identifier这样的标记来标志c的函数名是一件繁琐的事情.

可以通过安装第三方的包,让我们从琐碎中解放出来.在命令行下运行如下命令就可以了

1
$ raco pkg install ffi-definer-convention

然后是我们的代码部分,我们已经有这些代码了

 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
36
37
38
39
40
41
42
43
44
#lang racket
(require racket/draw
         ffi/unsafe
         ; avoid conflict with below
         (except-in ffi/unsafe/define
                    define-ffi-definer)
         ; the new 3rd-party pkg
         ffi-definer-convention
         pict)

; C types
(define-cpointer-type _cairo_t)
(define-cpointer-type _cairo_surface_t)
(define _cairo_line_cap_t
  (_enum '(butt round square)))

(define cairo-lib (ffi-lib #f))
(define-ffi-definer define-cairo cairo-lib
  ; describes how to transform from
  ; Racket to C ids
  #:make-c-id convention:hyphen->underscore)

; the foreign functions
; note lack of #:c-id keyword arguments
(define-cairo cairo-create
  (_fun _cairo_surface_t -> _cairo_t))
(define-cairo cairo-move-to
  (_fun _cairo_t _double _double -> _void))
(define-cairo cairo-line-to
  (_fun _cairo_t _double _double -> _void))
(define-cairo cairo-set-line-width
  (_fun _cairo_t _double -> _void))
(define-cairo cairo-stroke
  (_fun _cairo_t -> _void))
(define-cairo cairo-set-line-cap
  (_fun _cairo_t _cairo_line_cap_t -> _void))

; (_cairo_t -> Void) -> Pict
; do some drawing and give us the pict
(define (do-cairo f)
  (define bt (make-bitmap 256 256))
  (define bt-surface (send bt get-handle))
  (f (cairo-create bt-surface))
  (linewidth 2 (frame (bitmap bt))))

注意,这里define-cairo函数不再需要#:c-id identifier这样的标志了.这里的define-ffi-definer是来自第三方的包ffi-definer-convention中.在一开始定义define-cairo的时候边设定好函数名的转换规则,这里的转换规则是从连字符到下划线,-_.

而且,此处定义了一个函数do-cairo,这样我们也不用再去自己定义一个bitmap了.do-cairo将会接受一个绘制函数,在bt中绘制出来并显示.

正章

准备得差不多是时候上硬菜了(我不是东北人).这次我们试试看使用Cairo库中的path对象.首先看看它是怎么定义的

1
2
3
4
5
typedef struct {
    cairo_status_t status;
    cairo_path_data_t *data;
    int num_data;
} cairo_path_t;

为了能使用path,我们需要定义一个Racket的FFI的C数据类型与之对应.在这之前需要先对path的成员变量类型定义

1
(define _cairo_status_t _int)

这实际上是个枚举类型,在这篇博文中我们并不在乎怎么区分不同的状态.第二个成员变量类型实际是一个数组类型,数组中放的是path的data对象.而cairo_path_data_t是一个联合体,其定义如下

1
2
3
4
5
6
7
8
9
union _cairo_path_data_t {
    struct {
        cairo_path_data_type_t type;
        int length;
    } header;
    struct {
        double x, y;
    } point;
};

FFI当然可以方便的支持结构体了.使用_union构造函数即可._union构造函数可以接受任意数量的参数,每一个都是这个联合体的一种子情况.所以在Racket中定义一个联合体easy.

1
2
3
4
5
6
7
8
9
;这就是一个枚举类型
(define _cairo_path_data_type_t
    (_enum '(move-to line-to curve-to close-path)))
(define _cairo_path_data_t
    (_union ; the header case
            (_list-struct _cairo_path_data_type_t
                          _int)
            ; the point case
            (_list-struct _double _double)))

这里有一个从未见过的构造函数_list-struct.此构造函数可以在c和racket之间传递一个结构体,此结构体成员变量的数据类型紧随其后.文档上说这不是一个高效的函数.如果对于效率有要求的话可以使用define-cstruct.并且这个构造函数并不会像define-cstruct那样附带定义选择函数,谓词函数等.不过你可以像一个普通的列表那样操作这个数据类型.

联合体使用起来比较繁复,因为在Racket这边你必须要手动区分不同的情况.我们用一些代码来演示这个问题:

1
2
3
4
5
6
7
8
9
; 用一个double型的list构建一个联合体
> (define a-union-val
    (cast (list 1.3 5.8)
          ; source type
          (_list-struct _double _double)
          ; target type
          _cairo_path_data_t))
> a-union-val
#<union>

这个小例子中用到cast来构建一个_cairo_path_data_t.cast可以将一个c数据类型强制转换为另一个.因为使用cast构建一个联合体比较方便所以在这个例子中用到了它.

从第二行的输出可以看出,我们并不知道一个联合体里面放的到底是什么.当然你可以使用union-ref函数取出联合体里面的东西但是这是不安全的,因为你无法确定里面的数据类型,所以取出的数据可能是无效的数据.

据个例子来说明一下

1
2
3
4
5
6
7
8
; 正确的打开方式
; 各种情况的顺序由定义时确定,序号从0开始
> (union-ref a-union-val 1)
'(1.3 5.8)
; 错误的打开方式
> (union-ref a-union-val 0)
enum:int->_cairo_path_data_type_t: expected a known
#<ctype:ufixint>, got: 3435973837

这里错误的打开方式racket给我们报错了.这次比较幸运因为在通常情况下你只会得到一个毫无意义的数据,并不会报错.这时候人生就比较悲剧了.查错是十分困难的.

像这样的联合体通常都会有办法确定联合体里面到底放的是那种数据类型的数据.有可能在c那边会额外增加一个结构体或者值来说明这个值的数据类型.或者是某些设定说明了结构体里面放的是什么.原文Alternatively, there may be some set order that cases appear in data structures.

对于Cairo的api而言数组中的头元素就可以用来确定联合体中存放的值的数据类型.

在继续之前先总结一下,我们已经有一个用来描述path data的联合体,接下来看看怎么处理数组.

一些底层操作

由于我们还没有与c对应的cairo_path_t,马上来定义一个好了.

1
2
3
4
(define _simple_cairo_path_t
    (_list-struct _cairo_status_t
                  _pointer
                  _int)) 

在这里我们只是使用指针_pointer 来表示数组.为了安全性可以考虑使用 ’_cpointer cairo_status_t>,这样就申明了一个标记了的c指针了,(好像对c的指针打上标记以将其和其他的c指针区分开来那样).

我们之前也见过这种指针类型的数据类型,但是除了将它作为参数传递以外什么也没做.实际上指针类型也是可以被操作的,除了作为参数简单的传递之外.

在我们这么做之前先将cairo_copy_path进行绑定,这样我们才能去操作一个路径的结构体

1
2
(define-cairo cairo-copy-path
    (_fun _cairo_t -> _pointer))

来试试看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(define a-path #f)

(do-cairo (λ (ctx)
              ; Do stuff to make the current
              ; path non-empty
              (cairo-move-to ctx 50.0 50.0)
              (cairo-line-to ctx 206.0 206.0)
              (cairo-move-to ctx 50.0 206.0)
              (cairo-line-to ctx 115.0 115.0)
              ; Get the current path
              (set! a-path (cairo-copy-path ctx))
              ; Stroke clears the path
              ; so do it last
              (cairo-stroke ctx)))

let’s run it

画出来一个λ ,这次再看看a-path被设置成什么了.

1
2
> a-path
#<cpointer>

记住,cairo-copy-path仅仅给我们一个指向path结构体的指针并不是直接一个结构体.因此我们需要知道如何使用一个指针类型的数据.对于指针最有用的函数是ptr-ref.可以让你取消引用一个指针进而得到具体的c类型.

注意:ptr-ref也可以接受一个可选的偏移量参数,这会在稍后的例子中提到.

例如,我们可以将a-path当做是_sample_cairo_path_t来使用:

1
2
3
4
> (define simple-path
    (ptr-ref a-path _simple_cairo_path_t))
> simple-path
'(0 #<cpointer> 8)

现在我们已经有了这个指针所指向的结构体的Racket表示了.but 这里的data成员也是一个指针(代表的是数组).为了将他转化为一个更加有用的数据,我们将对数组再次使用ptr-ref.

1
2
3
4
5
6
(define array
  (ptr-ref ; the pointer
       (second simple-path)
       (_array/list _cairo_path_data_t
                    ; length field
                    (third simple-path))))

看看array中的内容

1
2
> array
'(#<union> #<union> #<union> #<union> #<union> #<union> #<union> #<union>)

正如我们所期待的那样,这个array中都是联合体类型.联合体还是有点讨厌的,我们必须要知道其中存放的到底是哪一种数据类型,才能正确地取出数据.

1
2
3
4
5
6
7
> (union-ref (first array) 0)
'(move-to 2)
> (union-ref (second array) 1)
'(50.0 50.0)
; 使用了错误的数据类型,从联合体中取出了无效的数据
> (union-ref (third array) 1)
'(4.2439915824246e-314 3.7548080003146e-317)

我们可以写一个辅助函数.将数组类型转化为更加有用的形式,这里可以从下标推断出正确的参数总是010101,间隔的(比如,first对应0 ,second对应1,third应该对应0,fourth应该对应1…..) 注意这并非原文的解释,是我个人的理解,不知道是否正确

另一种选择是自定义一种c的数据类型,自动为我们做这样的转化.这样就可以直接使用FFI的绑定的函数而不用考虑辅助函数了.

自定义c数据类型

在第一篇博文中我简要地提到过如何创建一个自定义c数据类型,但是这次我们更加仔细地过一遍.构建一个自定义c数据类型需要一个基础的c类型数据和两个转化函数,一个转化函数将Racket类型转化为c类型,另一个正好倒过来,将c类型的数据转化为Racket类型.

这样就有可能进行有趣的转化,比如将一个联合体自动地正确第拿出来.

我们来定义一个Cairo的path类型.path=一个序列化的元素表,元素=动作+动作的参数

首先用Racket来定义一个path类型

1
2
3
(struct cairo-path (ptr)
    #:property prop:sequence
    (λ (p) (in-cairo-path p)))

这个定义中定义了ptr正如其名我们会在这里放一个指针,至于怎么使用我们之后再将

这里使用到了结构体的属性设置使得实例化的cairo-path可以自动序列化.这意味着你可以使用for循环来遍历这个序列或者使用sequence-ref直接访问某个元素. 这个属性通过一个函数来实现,此函数接受一个p(这里指实例化后元素自身).然后返回一个序列.

我们之后在定义这个in-cairo-path函数为我们自动的构建出相应的序列.现在看看对于这个结构体如何创建其c类型的定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(define _cairo_path_t
    (let ()
      ; Extract pointer out of representation
      (define (racket->c rkt)
        (cairo-path-ptr rkt))
      ; Just apply the Racket constructor
      (define (c->racket cobj)
        (cairo-path cobj))
      (make-ctype _pointer
                  racket->c
                  c->racket)))

这里的基础数据类型是_pointer,因为Cairo的API返回一个指针类型来代表,所以不可避免的需要用到指针. 从c到racket我们简单地将指针塞进了cairo-path构造函数.从racket将path类型转化为指针.

真正的工作由in-cairo-path完成的

遵循自顶向下的设计模式,我们来看看in-cairo-path的定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
; Cairo-Path -> Sequence 将路径转化为序列
(define (in-cairo-path path)
    (define pp (cairo-path-ptr path))
    (match-define
      (list _ array-ptr len)
      (ptr-ref pp _simple_cairo_path_t))
    (make-do-sequence
      (λ ()
        (values (pos->element array-ptr)
                (next-pos array-ptr)
                0
                (λ (pos) (< pos len))
                #f #f))))

cairo-path-prt是定义cairo-path的副产品.拿到path后直接将其转化为指针,通过match-define取出其中的路径部分,再将其序列化.

在从path对象中提取出数组和长度对象之后,我们将他们传递给一些辅助函数来实现序列化.通常定义一个序列的方式就是使用make-do-sequence函数.实质上make-do-sequence需要一堆参数来确定如何从序列中取出元素,如何推进序列,如何开始序列,如何结束序列.

注意:从技术上来说,make-do-sequence函数接受的是一个形实替换程序(thunk),这个替换程序会产生一系列的值.这些值的作用就和参数一样.为什么这是一个替换程序是因为在序列开始之前你也许会需要运行一些初始化的代码(e.g. 打开一个网络连接).你的序列函数(如何推进一个序列)也会会建立在初始化之后的结果上.

在这里我们提供了一些柯里化的函数从底层的c数组中取出我们需要的元素.pos->element以及它的辅助函数就是干这个的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
; CPointer -> Integer -> Element
(define ((pos->element ptr) pos)
    ; Extract the data path header
    (define header
      (union-ref
       (ptr-ref ptr _cairo_path_data_t pos)
       0))
    (define type   (first header))
    ; Length includes header, so subtract 1
    (define len    (sub1 (second header)))
    (define pos*   (add1 pos))
    (define points (get-points ptr pos* len))
    (cons type points))
; CPointer Integer Integer -> (Listof Data)
(define (get-points ptr pos num-points)
    (for/list ([i (in-range num-points)])
      (union-ref (ptr-ref ptr
                          _cairo_path_data_t
                          ; offset argument
                          (+ pos i))
                 1)))

这里涉及到Cairo指定的api使用方式.header元素后面跟着一些data元素.header元素指定长度,所以我们才能循环的取出数据,直到序列结束,每一步都需要将union对象取消应用将其中的正真的值取出.

推进序列比较简单,我们要做的只是,根据头元素指定的长度做一些算数运算而已.

1
2
3
4
5
6
7
(define ((next-pos ptr) pos)
    (define header
      (union-ref
       (ptr-ref ptr _cairo_path_data_t pos)
       0))
    (define len (second header))
    (+ len pos))

打完收工. racket (define-cairo cairo-copy-path (_fun _cairo_t -> _cairo_path_t))

将path对象当做序列使用 racket (do-cairo (λ (ctx) (cairo-move-to ctx 50.0 50.0) (cairo-line-to ctx 206.0 206.0) (cairo-move-to ctx 50.0 206.0) (cairo-line-to ctx 115.0 115.0) (define path (cairo-copy-path ctx)) ; Using path as a sequence (for ([elem path]) (displayln elem)) (cairo-stroke ctx)))

最终的效果如下

完整的代码如下,有点长了

  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
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#lang racket
(require racket/draw
         ffi/unsafe
         ; 避免产生冲突
         (except-in ffi/unsafe/define
                    define-ffi-definer)
         ; the new 3rd-party pkg
         ffi-definer-convention
         pict)

; C types
(define-cpointer-type _cairo_t)
(define-cpointer-type _cairo_surface_t)
(define _cairo_line_cap_t
  (_enum '(butt round square)))

(define cairo-lib (ffi-lib #f))
(define-ffi-definer define-cairo cairo-lib
  ; describes how to transform from
  ; Racket to C ids
  #:make-c-id convention:hyphen->underscore)

; the foreign functions
; note lack of #:c-id keyword arguments
(define-cairo cairo-create
  (_fun _cairo_surface_t -> _cairo_t))
(define-cairo cairo-move-to
  (_fun _cairo_t _double _double -> _void))
(define-cairo cairo-line-to
  (_fun _cairo_t _double _double -> _void))
(define-cairo cairo-set-line-width
  (_fun _cairo_t _double -> _void))
(define-cairo cairo-stroke
  (_fun _cairo_t -> _void))
(define-cairo cairo-set-line-cap
  (_fun _cairo_t _cairo_line_cap_t -> _void))

; (_cairo_t -> Void) -> Pict
; do some drawing and give us the pict
(define (do-cairo f)
  (define bt (make-bitmap 256 256))
  (define bt-surface (send bt get-handle))
  (f (cairo-create bt-surface))
  (linewidth 2 (frame (bitmap bt))))

(define _cairo_status_t _int)
(define _cairo_path_data_type_t
  (_enum '(move-to line-to curve-to close-path)))
(define _cairo_path_data_t
  (_union
   ;;第一种情况
   (_list-struct _cairo_path_data_type_t
                 _int)
   ;the point case
   (_list-struct _double _double)))

(define _simple_cairo_path_t
  (_list-struct _cairo_status_t
                _pointer
                _int))

;; (define a-path #f)

;; (do-cairo (λ (ctx)
;;             ;;确保当前的路径非空
;;             (cairo-move-to ctx 50.0 50.0)
;;             (cairo-line-to ctx 206.0 206.0)
;;             (cairo-move-to ctx 50.0 206.0)
;;             (cairo-line-to ctx 115.0 115.0)
;;             ;;得到当前的路径
;;             (set! a-path (cairo-copy-path ctx))
;;             ;;画出路径,所以最后才做
;;             (cairo-stroke ctx)
;;             ))


;; (define simple-path
;;   (ptr-ref a-path _simple_cairo_path_t))




;; (define array
;;   (ptr-ref ;指针
;;    (second simple-path)
;;    (_array/list _cairo_path_data_t
;;                 ;;长度
;;                 (third simple-path))))


(struct cairo-path (ptr)
  #:property prop:sequence
  (λ (p) (in-cairo-path p)))


(define _cairo_path_t
  (let ()
    ;;使用指针来表示
    (define (racket->c rkt)
      (cairo-path-ptr rkt))
    ;;直接使用Racket的构造函数
    (define (c->racket cobj)
      (cairo-path cobj))
    (make-ctype _pointer
                racket->c
                c->racket)))

(define-cairo cairo-copy-path
  (_fun _cairo_t -> _cairo_path_t))


(define (in-cairo-path path)
  (define pp (cairo-path-ptr path))
  (match-define
    (list _ array-ptr len)
    (ptr-ref pp _simple_cairo_path_t))
  (make-do-sequence
   (λ ()
     (values (pos->element array-ptr)
             (next-pos array-ptr)
             0
             (λ (pos) (< pos len))
             #f #f))))


(define ((pos->element ptr) pos)
  ;;取出数据路径的头部
  (define header
    (union-ref
     (ptr-ref ptr _cairo_path_data_t pos)
     0))
  (define type (first header))
  ;;长度包括了头部所以要减一
  (define len (sub1 (second header)))
  (define pos* (add1 pos))
  (define points (get-points ptr pos* len))
  (cons type points))

(define (get-points ptr pos num-points)
  (for/list ([i (in-range num-points)])
    (union-ref (ptr-ref ptr
                        _cairo_path_data_t
                        ;;偏移量参数
                        (+ pos i))
               1)))
(define ((next-pos ptr) pos)
  (define header
    (union-ref
     (ptr-ref ptr _cairo_path_data_t pos)
     0))
  (define len (second header))
  (+ len pos))


;; (define-cairo cairo-copy-path
;;   (_fun _cairo_t -> _cairo_path_t))



(do-cairo (λ (ctx)
            (cairo-move-to ctx 50.0 50.0)
            (cairo-line-to ctx 206.0 206.0)
            (cairo-move-to ctx 50.0 206.0)
            (cairo-line-to ctx 115.0 115.0)
            (define path (cairo-copy-path ctx))
            ; Using path as a sequence
            (for ([elem path])
              (displayln elem))
            (cairo-stroke ctx)))