FIELD NOTES: 書を持って街へ出よう

合同会社フィールドワークス プログラマ兼代表のブログ

OCamlでPython拡張モジュールを作る

OCamlで作成したライブラリをPythonから利用できるように,拡張モジュール化した。その際に,いくつか苦労したので記録に残しておく。
なお開発環境は,Mac OS X 10.6.4 (Snow Leopard), Ocaml 3.12.0, Python 2.6.5 である。

一般的な作り方

mBの通り作業すれば,サンプルのような単純なOCaml関数をPythonから呼び出すことができるようになる。
しかし,実用的なプログラムでは外部のライブラリを必ず利用することになるので,どこかで拡張モジュールにライブラリをリンクしなければならない。

OCamlコードコンパイル時の注意

C言語形式のオブジェクトファイルを出力するには,-output-objオプションをつけてOCamlコードをコンパイルすれば良い。しかし,既存のfoo.cmxaライブラリが存在するとして,すべて-output-objオプションを付けて再コンパイルするのは現実的でない。また,サードパーティ製のライブラリを使用している場合は,最悪ソースコードが手に入らない場合も考えられる。
そこで,拡張モジュールとして公開する関数のみを集めたOCamlソースコードbar.mlを作成し,このファイルのみを-output-objオプションを付けてコンパイルすることにする。
また,-output-objオプションを付けた場合,引数で指定した既存ライブラリもCオブジェクトに変換してまとめて出力してくれるようだ。
以下に,コマンドラインのイメージを示す。

ocamlopt -output-obj -o bar.o foo.cmxa bar.ml

リンク時の注意

Cライブラリの関数を呼び出すことで実際の処理を実現しているライブラリが存在する。そのようなライブラリを使用している場合には,拡張モジュールをリンクする際に対応するCライブラリをリンクしてやる必要がある。
例えば,以下のOCaml標準ライブラリとCライブラリの対応は次のようになる。

String → libcamlstr.a (Ocaml 3.12以前はlibstr.a?)
Unix → libunix.a
Bigarray → libbigarray.a

はまり点1

実際のプロジェクトではjson-wheelというJSONライブラリを使用していたのだが,json-wheelは依存するライブラリが多く,必要なライブを上記のようにコンパイル時・リンク時に次々に追加していった。

json-wheel → netstring → pcre, netsys, unix

ところが,最終的に _pcre_callout というシンボルが解決できず,Pythonからライブラリをimportする際にエラーになる。

>>> import bar
ImportError: dlopen(./bar.so, 2): Symbol not found: _pcre_callout

PCREはPerl互換の正規表現ライブラリだが,JSONで正規表現が必要な場面が思いつかない。そこでjson-wheelのソースを調べてみると,なんとnetstringに依存しているのは1箇所のみであった。それは,UnicodeUTF-8文字列に変換する関数だったので,その部分を手作りのコードに置き換えて,json-wheelからnetstringやpcreへの依存をなくすことで問題を回避した。
念のため,json-wheelへのパッチを以下に記す。

diff -Naru json-wheel.org/META json-wheel/META
--- json-wheel.org/META	2009-01-15 23:33:53.000000000 +0900
+++ json-wheel/META	2010-08-31 17:35:42.000000000 +0900
@@ -1,6 +1,6 @@
 name = "json-wheel"
 version = "1.0.6"
 description = "JSON data format"
-requires = "netstring"
+requires = ""
 archive(byte)    = "jsonwheel.cma"
 archive(native)  = "jsonwheel.cmxa"
diff -Naru json-wheel.org/json_lexer.mll json-wheel/json_lexer.mll
--- json-wheel.org/json_lexer.mll	2009-01-15 23:33:53.000000000 +0900
+++ json-wheel/json_lexer.mll	2010-08-31 17:30:41.000000000 +0900
@@ -29,8 +29,27 @@
       if big_int_mode then STRING s
       else json_error (s ^ " is too large for OCaml's type int, sorry")
 
+
   let utf8_of_point i =
-    Netconversion.ustring_of_uchar `Enc_utf8 i
+    let implode l =
+      let b = Buffer.create (List.length l) in
+        List.iter (Buffer.add_char b) l;
+        Buffer.contents b
+    in
+    if i <= 0x007F then
+      implode [char_of_int i]
+    else if i <= 0x07FF then
+      implode [char_of_int (((i lsr  6) land 0x1F) lor 0xC0);
+               char_of_int ((i land 0x3F) lor 0x80)]
+    else if i <= 0x0000FFFF then
+      implode [char_of_int (((i lsr 12) land 0x0F) lor 0xE0);
+               char_of_int (((i lsr  6) land 0x3F) lor 0x80);
+               char_of_int ((i land 0x3F) lor 0x80)]
+    else
+      implode [char_of_int (((i lsr 18) land 0x07) lor 0xF0);
+               char_of_int (((i lsr 12) land 0x3F) lor 0x80);
+               char_of_int (((i lsr  6) land 0x3F) lor 0x80);
+               char_of_int ((i land 0x3F) lor 0x80)]
 
   let custom_error descr lexbuf =
     json_error 

はまり点2

これはMac OS X特有の現象と思われるが,_environというシンボルが解決できなくてimportエラーが発生した。

>>> import bar
ImportError: dlopen(./bar.so, 2): Symbol not found: _environ

EPICS environ symbol on MacOS X with EPICS base 3.14.10によると,次のようなことらしい。

  • Mac OS Xでは,_environは/usr/lib/crt1.oで定義されている。
  • Python拡張モジュール作成時には,crt1.oではなく対応する/usr/lib/dylib1.oをリンクしているが,dylib1.oでは_environを定義していない。
  • Mac OS Xで _environ の値が必要な場合には,*_NSGetEnviron()関数を呼び出せば良い。

仕方がないのでOCaml 3.12.0のソースを入手して,unixライブラリに以下の修正を施した。

diff -Naru ocaml-3.12.0.org/otherlibs/unix/envir.c ocaml-3.12.0/otherlibs/unix/envir.c
--- ocaml-3.12.0.org/otherlibs/unix/envir.c	2005-03-25 02:20:54.000000000 +0900
+++ ocaml-3.12.0/otherlibs/unix/envir.c	2010-09-01 10:44:56.000000000 +0900
@@ -16,7 +16,12 @@
 #include <mlvalues.h>
 #include <alloc.h>
 
-#ifndef _WIN32
+#if defined(__APPLE__)
+#  include <crt_externs.h>
+#  define environ (*_NSGetEnviron())
+#endif
+
+#if !defined(_WIN32) && !defined(__APPLE__)
 extern char ** environ;
 #endif
 
diff -Naru ocaml-3.12.0.org/otherlibs/unix/execvp.c ocaml-3.12.0/otherlibs/unix/execvp.c
--- ocaml-3.12.0.org/otherlibs/unix/execvp.c	2010-01-22 21:48:24.000000000 +0900
+++ ocaml-3.12.0/otherlibs/unix/execvp.c	2010-09-01 10:45:23.000000000 +0900
@@ -18,7 +18,13 @@
 #include "unixsupport.h"
 
 extern char ** cstringvect();
-#ifndef _WIN32
+
+#if defined(__APPLE__)
+#  include <crt_externs.h>
+#  define environ (*_NSGetEnviron())
+#endif
+
+#if !defined(_WIN32) && !defined(__APPLE__)
 extern char ** environ;
 #endif