Py + GI - 高效 C 庫整合方案

Overview

想用 Python 調用 C libraries 有不少途徑 (中間需要開發層 binding), 其中我的最愛是配合 GObject Introspection (GI) 項目.

配合 GI, 針對新語言, 只要跟 GI 整合一次, 之後通過新增 type library 即可不斷擴充功能, 跟一般 binding 的差異為

  • 以工具從 header files 中抽出
  • 額外的資訊以 C 註釋 annotation 方式呈現
  • type libraries 是 data, 非 code
  • 可共用於不同程序語言
  • 易於整合在 upstream 項目中, 避免版本匹配的問題

Python 己有 PyGObject 項目實現了與 GI 的整合, 因此 language binding 的開發可以省下來. 重心只需要放在 type library 的產生, 過程大致如下

  1. 開發/驗証 library
  2. 如果有必要, 在 header 中加上給 GI 看的 annotation
  3. g-ir-scanner parse header 產生 .gir, 文本型態的 type information, 可供工具整合用
  4. g-ir-compiler 產生 .typelib (type library), binary 型態的 type information
  5. 以目標語言進行整合測試

Install Prerequisites

要 build & run 以下 demo, 開發環境需要先裝上以下 packages

$ apt-get install build-essential gobject-introspection python-gobject libglib2.0-dev gtk-doc-tools

Hello

一個最簡單的例子如下

/* mylib.h */
#ifndef __MYLIB_H_
#define __MYLIB_H_

void hello();

#endif /* __MYLIB_H_ */
/* mylib.c */
#include "mylib.h"
#include <stdio.h>

void hello()
{
    printf("hello\n");
}
# Makefile
CFLAGS :=-fPIC

MyLib-1.0.typelib: MyLib-1.0.gir
        g-ir-compiler MyLib-1.0.gir >MyLib-1.0.typelib

MyLib-1.0.gir: mylib.h libmylib.so
        LD_LIBRARY_PATH=$(PWD) \
                g-ir-scanner \
                        -L. \
                        -lmylib \
                        -n MyLib \
                        --accept-unprefixed \
                        --nsversion=1.0 \
                        -o MyLib-1.0.gir \
                        --warn-all \
                        mylib.h

libmylib.so: mylib.o
        gcc -o [email protected] $< -shared -fPIC

mylib.o: mylib.c

.PHONY: run
run:
        GI_TYPELIB_PATH=$(PWD) \
        LD_LIBRARY_PATH=$(PWD) \
                python \
                -c 'from gi.repository import MyLib; MyLib.hello()'

.PHONY: clean
clean:
        rm -rf *.o *.so *.gir *.typelib

上面 3 個檔保存後, 執行 make

cc -fPIC   -c -o mylib.o mylib.c
gcc -o libmylib.so mylib.o -shared -fPIC
LD_LIBRARY_PATH=/home/derekdai/test-gi \
        g-ir-scanner \
                -L. \
                -lmylib \
                -n MyLib \
                --accept-unprefixed \
                --nsversion=1.0 \
                -o MyLib-1.0.gir \
                --warn-all \
                mylib.h
g-ir-scanner: compile: cc -Wno-deprecated-declarations -pthread -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -c -o /home/derekdai/test-gi/tmp-introspectby3_Ty/MyLib-1.0.o /home/derekdai/test-gi/tmp-introspectby3_Ty/MyLib-1.0.c
g-ir-scanner: link: libtool --mode=link --tag=CC cc -o /home/derekdai/test-gi/tmp-introspectby3_Ty/MyLib-1.0 -export-dynamic /home/derekdai/test-gi/tmp-introspectby3_Ty/MyLib-1.0.o -L. -lmylib -L. -lgio-2.0 -lgobject-2.0 -Wl,--export-dynamic -lgmodule-2.0 -pthread -lglib-2.0
libtool: link: cc -o /home/derekdai/test-gi/tmp-introspectby3_Ty/MyLib-1.0 /home/derekdai/test-gi/tmp-introspectby3_Ty/MyLib-1.0.o -Wl,--export-dynamic -pthread -Wl,--export-dynamic  -L. -lmylib -lgio-2.0 -lgobject-2.0 -lgmodule-2.0 -lglib-2.0 -pthread
g-ir-compiler MyLib-1.0.gir >MyLib-1.0.typelib

Makefile 中會先 build 出 libmylib.so, 配合著它及 header files 讓 g-ir-scanner 產生出 .gir, 內容如下

<?xml version="1.0"?>
<!-- This file was automatically generated from C sources - DO NOT EDIT!
To affect the contents of this file, edit the original C definitions,
and/or use gtk-doc annotations.  -->
<repository version="1.2"
            xmlns="http://www.gtk.org/introspection/core/1.0"
            xmlns:c="http://www.gtk.org/introspection/c/1.0"
            xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
  <namespace name="MyLib"
             version="1.0"
             shared-library="libmylib.so"
             c:identifier-prefixes="MyLib"
             c:symbol-prefixes="my_lib">
    <function name="hello" c:identifier="hello">
      <return-value transfer-ownership="none">
        <type name="none" c:type="void"/>
      </return-value>
    </function>
  </namespace>
</repository>

然後再以 g-ir-compiler 產生 .typelib 檔, 此時目錄內容如下

$ ls
libmylib.so  Makefile  MyLib-1.0.gir  MyLib-1.0.typelib  mylib.c  mylib.h  mylib.o

要讓 python 能使用 libmylib.so, 需要指定 shared library (.so) 及 type library (.typelib) 所在位置 (如果已安裝到系統中默認路徑的話就不需要額外指定), 例如

$ GI_TYPELIB_PATH=$PWD \
  LD_LIBRARY_PATH=$PWD \
    python \
    -c 'from gi.repository import MyLib; MyLib.hello()'
hello

這樣就完成了第一個可供 python 使用的 C library.

Transferring Of Ownership

在 C 中沒有 garbage collector, 所以資源的控管要自行處理, 這塊在開發 binding 時也是必須多注意的. 例子如下

/* mylib.h */
#ifndef __MYLIB_H_
#define __MYLIB_H_

const char * hello3();

char * hello2();

#endif /* __MYLIB_H_ */
/* mylib.c */
#include "mylib.h"
#include <stdio.h>
#include <string.h>

const char * hello3()
{
    return "hello";
}

char * hello2()
{
    return strdup("hello");
}

`hello2()` 配置內存後 copy "hello" 進去, 所以傳回值用完要 `free()`. `hello3()` 則否, 看下產生的 .gir 有什麼差異

    ...
    <function name="hello2" c:identifier="hello2">
      <return-value transfer-ownership="full">
        <type name="utf8" c:type="char*"/>
      </return-value>
    </function>
    <function name="hello3" c:identifier="hello3">
      <return-value transfer-ownership="none">
        <type name="utf8" c:type="const char*"/>
      </return-value>
    </function>
    ...

對於 return value 為 char * 型態的, g-ir-scanner 會將它標記為 transfer-ownership="full", 也就是 caller 在用完 return value 後要負責 release 它. const char * 則否.

執行看看

GI_TYPELIB_PATH=$PWD \
LD_LIBRARY_PATH=$PWD \
        python \
        -c 'from gi.repository import MyLib; print MyLib.hello2(); print MyLib.hello3()'
hello
hello

如果 transfer-ownership= 設定不正確是否會有影響? 來實驗看看

/* mylib.h>
#ifndef __MYLIB_H_
#define __MYLIB_H_

/**
 * hello4:
 *
 * Returns: (transfer full):
 */
const char * hello4();

#endif /* __MYLIB_H_ */
/* mylib.c */
#include "mylib.h"
#include <stdio.h>

const char * hello4()
{
    return "hello";
}

上面 mylib.h 的注釋是所謂的 annotation, 用來提供 g-ir-scanner 額外資訊用, 此例我們強迫 g-ir-scannerhello4() 傳回值的 ownership 轉給 caller, 所以 caller 要負責 release, 執行看看

GI_TYPELIB_PATH=/home/derekdai/test-gi \
LD_LIBRARY_PATH=/home/derekdai/test-gi \
        python \
        -c 'from gi.repository import MyLib; print MyLib.hello4()'
*** Error in `python': munmap_chunk(): invalid pointer: 0x00007f4503efd65d ***
Aborted

Oops! 出問題啦~ 這個實驗告訴我們, 雖然 gi 方便, 一些如參數型態, 傳回值型態, function name 之類的都可以自動幫我們生成, 但碰到了與 default 行為不同的情況, 就必須注意 ownership 的管理, 不然就會發生 resource leak 或是 double free or 針對錯誤的 address 釋放, 程序也就不能正確的執行了.

Out Parameter

C function 的參數還會有 in/out/inout 等方向的差異, 例如

/* mylib.h */
#ifndef __MYLIB_H_
#define __MYLIB_H_

/**
 * hello5:
 *
 * @msg: (out callee-allocates):
 */
void hello5(char **msg);

#endif /* __MYLIB_H_ */
#include "mylib.h"
#include <stdio.h>
#include <string.h>
#include <assert.h>

void hello5(char **msg)
{
    assert(msg != NULL && *msg == NULL);
    *msg = strdup("hello");
}

hello5() 有個 out 參數, 放置由 hello5() 配置的 "hello", 所以在 .h 中我們加上了 @msg: (out callee-allocates):. 但因為 Python 的參數沒有 out 類型, 所以在 PyGObject 中會是跟 return value 混在一塊以 tuple 的方式傳回, 調用方式如下

$ GI_TYPELIB_PATH=$PWD LD_LIBRARY_PATH=$PWD python -c 'fro
m gi.repository import MyLib; print MyLib.hello5()'                                   
hello

Legacy Structure

如果 function 會傳回 struct, 這時就需要額外做一些事

  • 提供一個 new function, 供配置及初始 struct 的 instance
  • 提供一個 *_get_type() function, 在其中向 GObject type system 註冊一個 boxed type
  • 提供一個 *_copy() function 在 GObject type system 需要時複製一份目前的 instance
  • 提供一個 *_free() function 在 instance 用完後 release 掉

在 struct 中沒有 pointer, 或是 struct 本身自帶 reference counter 的情況, 以上 function 基本都可以自行開發小工具自動產生. 來看個例子

#ifndef __MYLIB_H_
#define __MYLIB_H_

#include <glib-object.h>

#define TYPE_MY_OBJ         (my_obj_get_type())

typedef struct _MyObj MyObj;

MyObj my_obj_new();

GType my_obj_get_type();

#endif /* __MYLIB_H_ */
#include "mylib.h"
#include <stdlib.h>

struct _MyObj
{
    int v;
};

MyObj * my_obj_new()
{
    return malloc(sizeof(MyObj));
}

MyObj * _my_obj_copy(MyObj *self)
{
    MyObj *new_self = my_obj_new();
    *new_self = *self;

    return new_self;
}

void _my_obj_free(MyObj *self)
{
    free(self);
}

GType my_obj_get_type()
{
    static GType type = 0;
    if(! type) {
        type = g_boxed_type_register_static("MyObj",
                                            (GBoxedCopyFunc) _my_obj_copy,
                                            (GBoxedFreeFunc) _my_obj_free);
    }

    return type;
}

C code 應該是相當好理解的, 只有 2 點需要說明一下

  • function 前要有 prefix, 例如 MyObj 的 function prefix 為 my_obj, 如果有這樣的直接對應會是最省工的
  • g_boxed_type_register_static() 基本上就是告訴 GObject type system 說我要註冊個 type 名叫 "MyObj", 要 copy instance 時請調 _my_obj_copy(), 要 release 時請調 _my_obj_free()

執行看看

$ GI_TYPELIB_PATH=$PWD LD_LIBRARY_PATH=$PWD bpython
>>> from gi.repository import MyLib
>>> o = MyLib.MyObj()
>>> o
<MyObj at 0x27d39c0>
>>> o.copy()
<MyObj at 0x2a7b7e0>
>>> o = None
>>> 

Git Repository

以上例子都可以從 gitcafe 自行 clone 下來修改實驗, URL 如下
https://gitcafe.com/derekdai/py-gi-demo