About

无限接近 ObjC [2] 的道路中… 那么在解决了 NSException (见 [[/lisp/objc-ns-exception/][ObjC [1.5]​]]) 之后, 又是什么阻碍了我呢? 答案就是我引入的 libffi.

虽然现在错误捕获这一块已经是完全胜利了 (大概), 但是另一个奇怪的问题出现了: 用 cffi-libffi 调用的函数, 好像返回值不太正确. 那么该怎么办呢? 只好自己去实现了吧.

如何使用 libffi

尽管可以依赖网上搜的教程, libffi 文档, 或者可以参考 This Library is a Hidden Gem, Tsoding (bilibili), 或者可以直接问 AI.

可以问 AI, 但是问 AI 好像不太行

我猜这是可能是因为用的 macOS 内置的 libffi:

pkg-config libffi --cflags

所以导致了和 AI 常见的 libffi 有所出入… (当然, 更有可能是因为没啥训练语料, 所以 AI 在乱说, 所以更建议是问完 AI 之后自己再修改一下 – 不过试过几遍之后, 可能会发现还不如自己直接写 – 问 AI 也是要时间的嘛.

因为我的目标是要能够调用 ObjC 中的方法, 所以这里尝试给 ObjC 中的 Type Encoding 都尽可能地测一边: 该如何调用 ObjC encoding.

ObjC encodingffi_type
:charffi_type_sint8
:unsigned-char, :boolffi_type_uint8
:intffi_type_sint32
:unsigned-intffi_type_uint32
:shortffi_type_sint16
:unsigned-shortffi_type_uint16
:long, :long-longffi_type_sint64
:unsigned-long, :unsigned-long-longffi_type_uint64
:floatffi_type_float
:doubleffi_type_double
:string, :object, :class, :sel, :unknownffi_type_pointer
:voidffi_type_void
:structffi_type_struct (should manually specify)
:union?
:bits?
:array?

使用 man ffi_call 命令可以获得一个最简单的调用的例子, 这里仿照其结果以及用 在 [[/lisp/objc-ns-exception/][ObjC [1.5]​]] 中提到的 coca_objc_msgSend 思路实现的一个 ffi_call wrapper 来作为例子.

点击展开折叠的例子
#import <Foundation/Foundation.h>
#include <ffi.h>

void coca_objc_msgSend (ffi_cif *cif, IMP imp, void* retval, void** args) {
  NSLog(@"coca_objc_msgSend");
  @try {
    ffi_call(cif, FFI_FN(imp), retval, args);
  }
  @catch (NSException *e) {
    NSLog(@"Safe: %@", e);
  }
}

int main (int argc, char **argv) {
  <<ffi_calling_code_here>>
}

不过这里更加推荐直接查看 objc/wrapper.lisp 中的代码作为默认的策略.

Basic Types and their return values

Trivial C Example

这里为了方便, 可以来看这样的一个例子:

#import <Foundation/Foundation.h>
#include <ffi.h>

void foo (char c, BOOL B, int i, unsigned int I, long l, long long q, float f, double d) {
  NSLog(@"Got %c, %d, %d, %u, %ld, %lld, %f, %f", c, B, i, I, l, q, f, d);
}

int main (int argc, char** argv) {
  ffi_cif cif;
  ffi_type* arg_types[8];

  arg_types[0] = &ffi_type_sint8;
  arg_types[1] = &ffi_type_uint8;
  arg_types[2] = &ffi_type_sint32;
  arg_types[3] = &ffi_type_uint32;
  arg_types[4] = &ffi_type_sint64;
  arg_types[5] = &ffi_type_uint64;
  arg_types[6] = &ffi_type_float;
  arg_types[7] = &ffi_type_double;

  if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 8, &ffi_type_void, arg_types) != FFI_OK)
    NSLog(@"Error!!!!!!");

  char             c = 'L';
  BOOL             B = YES;
  int              i = -23333;
  unsigned int     I = 3242342;
  long             l = -134533256432;
  long long        q = 3245633256732234543;
  float            f = 3.14159265354;
  double           d = 114.514;

  void* arg_values[8];
  arg_values[0] = &c;
  arg_values[1] = &B;
  arg_values[2] = &i;
  arg_values[3] = &I;
  arg_values[4] = &l;
  arg_values[5] = &q;
  arg_values[6] = &f;
  arg_values[7] = &d;

  ffi_call(&cif, FFI_FN(foo), NULL, arg_values);
}

大概演示了如何进行简单的 libffi 调用. 不难发现其实可以把上面的 main 函数过程看成两部分:

  • ffi_prep_cif: 准备函数类型签名部分
  • ffi_call: 实际函数调用部分

假如需要反复调用一个函数 – 这在 ObjC bridge 中应该是较常见的一种状态, 那么理论上可以通过复用 ffi_cif 来实现减少计算的功能. 实际上由于我们已知 ObjC method 的 type encoding, 根据 Type Encoding 来生成 ffi_cif 并非困难的事情.

如何生成…
(defun compile-objc-encoding-ffi-cif (encoding)
  (multiple-value-bind (arg-types ret)
      (decode-objc-type-encoding encoding)
    (let* ((cif    (foreign-alloc '(:struct ffi_cif)))
           (len    (length arg-types))
           (atypes (foreign-alloc :pointer :count len)))
      (loop :for i :from 0
            :for type :in arg-types
            :do (setf (mem-aref atypes :pointer i)
                      (objc-encoding-ffi-type type)))
      (assert (eq (ffi_prep_cif cif :default-abi len (objc-encoding-ffi-type ret) atypes)
                  :ffi-ok))
      cif)))

只是我们还是需要解决 struct 的难题…

Struct

修改以下上面的代码, 便可以得到:

#import <Foundation/Foundation.h>
#include <ffi.h>

typedef struct _greet {
  char *name;
  int repeat;
} Greet;

Greet foo (Greet greet) {
  for (int i = 0; i < greet.repeat; i++)
    NSLog(@"Hello %s", greet.name);
  return greet;
}

int main (int argc, char** argv) {
  ffi_cif cif;
  ffi_type ffi_type_greet;

  ffi_type* elements[3];

  elements[0] = &ffi_type_pointer;
  elements[1] = &ffi_type_sint32;
  elements[2] = NULL;

  ffi_type_greet.size      = 0;
  ffi_type_greet.alignment = 0;
  ffi_type_greet.type      = FFI_TYPE_STRUCT;
  ffi_type_greet.elements  = elements;

  ffi_type* arg_types[1];
  arg_types[0] = &ffi_type_greet;

  if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1, &ffi_type_greet, arg_types) != FFI_OK)
    NSLog(@"Error!!!!!!");

  Greet bar;
  bar.name = "Ryo";
  bar.repeat = 10;

  void* arg_values[1];
  arg_values[0] = &bar;

  Greet result;

  ffi_call(&cif, FFI_FN(foo), &result, arg_values);

  NSLog(@"Got: %s", result.name);
}

其实仍然不难发现, ffi_type_greet 也是一个可以被提前定义的量 – 在 define-objc-struct 的时候便可以被定义了.

不过好处是由于用的是 libffi, 现在并不需要过分手动展开结构体了, 只需要实现一个 objc-encoding-ffi-type 即可.

除了 Struct, 还有…

为了能够处理各种类型的输入和输出, 这里将整个调用过程的形式编译为如下的形式 (objc-method-foreign-call-form):

  1. foreign-alloc \(→\) arg_types
  2. 通过 objc-method-foreign-alloc-form 在堆上分配调用的参数的空间, 通过 objc-method-foreign-setf-form 将 Lisp 的值复制到堆上
  3. 调用设置好的 wrapper coca_objc_msgSend 来调用 ObjC method
  4. 通过 objc-method-foreign-aref-form 将结果从指针拷贝到 Lisp 环境中

如何使用 CFFI grovel

CFFI-Grovel 提供了一种非常巧妙的方法来构造 CFFI binding: 即通过生成一个 C 程序 (grovel), 将宏, 常量值, enum 等值打印输出为 Lisp 代码, 然后将这个 Lisp 代码作为代码载入; 以及通过编译一个 C 程序 (wrapper), 将一些简单的静态库链接编译成动态库载入, 同时也可以获得简单的 C 互操作性 – 我认为这比让用户手动打包一个 dylib 要优雅…

虽然但是, 在 CFFI 的文档和具体实现里面还是有些差别的… (见 PR). 等上游合并可能有点慢, 这里我选择了自己维护一份用于 ObjC 的 wrapper 拓展.

Grovel and Wrapper

实际的代码可以参考 grovel.lisp, 定义了 grovel file 和 wrapper file 是如何被映射为 C 代码, 以及如何生成 Lisp 端的接口的.

然后在 asdf.lisp 中注册了对应的功能来让 ASDF 载入的时候能够按照前面定义的规则进行处理. 实际上是非常棒的设计.

如何阅读 Lisp 代码

这里是一点点我的简单的阅读 Lisp 代码的小经验:

因为 Lisp 的动态性, 所以用 C 的那种 “静态” 代码分析其实并不是很现实 – 或者说, 会很痛苦, 所以最好还是利用 SLY/SLIME 这样的动态环境去分析代码的逻辑:

  • git clone ... 将代码拉到本地
  • (ql:quickload :system) 将系统载入
    • (trace ...): 通过 trace 来跟踪方法调用 – 其实更多的时候是用来不重定义函数, 来得到函数的输出和输入, 来确认函数的功能

      当然, 最好还是自己给这个函数加上注释和文档, 这样一来就方便自己下一次重新观察代码.

    • sly-edit-definition: 这样就能够直接跳到函数的定义
    • sly-who-calls, sly-who-binds, … 虽然有这些个功能, 但是貌似好多都跟踪不了 (找不到…
  • 并且在了解了一些 MOP 之后, 现在看到 Class 我也会觉得可以很好地阅读和了解了, 感觉就只是缺少一个比较好的可视化辅助 – LEM 的 Living Canvas 我觉得还挺不错, 可惜是在 Electron 上实现的, 希望 coca 实现完了之后能够摆脱 Electron 这个可怕的家伙…

ObjC File

如果对实际的实现感兴趣的话, 请参考 grovel/grovel.lisp 以及 grovel/asdf.lisp, 不过估计也不会有人那么闲吧… 那还是请参考 grovel/syntax.lisp 中具体的语法说明吧.

那么具体是什么呢?

grovel/asdf.lisp 中, 为 :objc-file 注册了一个处理接口, 于是所有的 :objc-file 将会被交由 process-objc-file 函数来进行处理.

对于 process-objc-file 函数, 其做的事情导致如下:

  • :objc-file 中读取 S-expr
  • 根据 S-expr 的 car 信息, 找到对应的 %process-objc-form 方法, 然后对其进行处理
  • grovel/syntax.lisp 中定义了一堆可用的处理函数
  • progn: 执行一组 ObjC form 指令 (实际上适合用来做 feature switch)
  • in-package: 设置 Lisp package
  • objc: 直接嵌入 ObjC 代码
  • cc-flags: 添加 compiler flags
  • ld-flags: 添加 linker flags
  • pkg-config-cflags: 使用 pkg-config ... --cflags 来添加 cc-flags
  • define: 映射到 C 的 #define ...
  • include: 映射到 C 的 #include <...>
  • import: 映射到 ObjC 的 #import <...>

之后我想我应该添加一些在 Coca.ObjC 包中的函数, 比如 define-objc-struct, define-objc-const 之类的命令来让写 ObjC wrapper 的体验和写 Lisp 代码是一模一样的.

最后

目前的状态是 #ad9c995, 我认为算是可以实现一个比较简单的 ObjC 调用了… 之后的目标还是继续学习 ObjC 和 Cocoa 的编程来实现最终的 macOS 的 GUI 编程的大业…