PythonからOCamlにアクセスするための低レベルインターフェースを作ってる

Python Developers Festaに参加しました。OCamlPythonの拡張モジュールを作っているという話をしていたら、何人かの人に興味を持ってもらって、色々と意見やアドバイスをいただきました。その時点ではPython/C APIOCaml/C Interfaceの橋渡しをCで実装していたのですが、ctypesでやってみるのはどうかという話があったので早速やってみました。
OCaml/C Interfaceの全機能の実装はまだ行っていませんが,ソースコードをBitbucketで公開しました。
https://bitbucket.org/likr/otypes

otypes

OCaml/C InterfaceのC関数部分はctypesから呼び出せるので問題はないのですが、他にも数々のマクロが定義されているのでそれらをctypesで地道に実装しています。otypes.pyの一部を抜粋して以下に掲載します。

#!/usr/bin/env python

import ctypes
import sys

class MLDLL(object):
    def __init__(self, name):
        self.lib = ctypes.CDLL(name)
        ArgArray = ctypes.c_char_p * (len(sys.argv) + 1)
        args = ArgArray()
        for i,arg in enumerate(sys.argv):
            args[i] = ctypes.c_char_p(arg)
        args[len(sys.argv)] = None
        self.lib.caml_startup(args)
        self.lib.caml_named_value.restype = ctypes.POINTER(ctypes.c_long)

    def caml_named_value(self, name):
        return self.lib.caml_named_value(ctypes.c_char_p(name)).contents
    
    def val_long(self, x):
        return (x << 1) + 1
    
    def long_val(self, x):
        return x.value >> 1
    
    def val_int(self, x):
        return self.val_long(x)
    
    def int_val(self, x):
        return self.long_val(x)

OCamlプログラムのビルド

以下のようなOCamlプログラムを用意してsample.mlとします。CやPythonなど外部からアクセスしたい値をCallback.registerで設定します。

let f x = x + 3

let sort_list list =
  List.sort (fun a  b -> b - a) list

let sort_array array =
  Array.iter (fun x -> Printf.printf "%d " x) array;print_newline ();
  Array.sort (fun a b -> b - a) array

let _ =
  Callback.register "x" 6;
  Callback.register "y" 7.;
  Callback.register "f" f;
  Callback.register "sort_list" sort_list;
  Callback.register "sort_array" sort_array;
  Callback.register "tuple" ('a','b');
  Callback.register "max3" (fun (a,b,c) -> max a (max b c))

ビルドには-output-objオプションを使ってCからリンク可能なsoファイルを作成します。

ocamlopt -c sample.ml
ocamlopt -output-obj -o sample.so sample.cmx

OCamlMakefileを使う場合は以下のようにMakefileを記述します。

SOURCES = sample.ml
RESULT  = sample.so

OCAMLLDFLAGS = -output-obj

all: native-code

include OCamlMakefile

OCamlプログラムのビルド(Mac

当方のMac環境では上記の方法ではctypesからのロード時に以下のようなエラーが発生して動かすことができませんでした。

importing mllibrary
Traceback (most recent call last):
  File "./test.py", line 7, in <module>
    lib = MLDLL('sample.so')
  File "/Users/likr/workspace/otypes/otypes.py", line 10, in __init__
    self.lib = ctypes.CDLL(name)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ctypes/__init__.py", line 353, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: dlopen(sample.so, 6): Symbol not found: _caml_atom_table
  Referenced from: /Users/likr/workspace/otypes/test/sample.so
  Expected in: flat namespace
 in /Users/likr/workspace/otypes/test/sample.so

試行錯誤してみたところ、ccオプションを指定してCコンパイラのオプションを外してやることで正しく動くようになりました。原因がしっかりとわかっていないので要調査です。

ocamlopt -cc="gcc" -output-obj -o sample.so sample.cmx

Makefileの場合はこうなります。

make OCAMLLDFLAGS='-output-obj -cc "gcc"'

サンプル

otypesを使ってPythonからsample.mlの値にアクセスしてみます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import otypes
from otypes import MLDLL

# ライブラリのロード
print 'importing mllibrary'
lib = MLDLL('sample.so')
print

# int値の取得
x = lib.caml_named_value('x')
print lib.int_val(x)
print

# int -> int 関数の呼び出し
f = lib.caml_named_value('f')
res = lib.caml_callback(f, x)
print lib.int_val(res)
print

# float値の取得
y = lib.caml_named_value('y')
print lib.double_val(y)
print

# float値の作成
d = lib.caml_alloc(1, otypes.DOUBLE_TAG)
lib.store_double_val(d, 0.3)
print lib.double_val(d)
print

# int * int の取得
ab = lib.caml_named_value('tuple')
print chr(lib.int_val(lib.field(ab, 0)))
print chr(lib.int_val(lib.field(ab, 1)))
print

# int * int * int -> int 関数の呼び出し
max3 = lib.caml_named_value('max3')
arg = lib.caml_alloc(3, 0)
lib.store_field(arg, 0, lib.val_int(3))
lib.store_field(arg, 1, lib.val_int(8))
lib.store_field(arg, 2, lib.val_int(5))
ret = lib.caml_callback(max3, arg)
print lib.int_val(ret)
print

# Arrayのソート
l = [1,4,2,35,64,3,12,14,46,57]
sort_array = lib.caml_named_value('sort_array')
ml_array = lib.caml_alloc(len(l), 0)
for i,item in enumerate(l):
    lib.store_field(ml_array, i, lib.val_int(item))
lib.caml_callback(sort_array, ml_array)
for i in range(len(l)):
    print lib.int_val(lib.field(ml_array, i))
print

# Listのソート
sort_list = lib.caml_named_value('sort_list')
def cons(hd, tl):
    l = lib.caml_alloc(2, 1)
    lib.store_field(l, 0, hd)
    lib.store_field(l, 1, tl)
    return l
tl = lib.val_emptylist()
tl = cons(lib.val_int(57), tl)
tl = cons(lib.val_int(46), tl)
l = cons(lib.val_int(58), tl)
l = lib.caml_callback(sort_list, l)
print lib.int_val(lib.field(l, 0))
l = lib.field(l, 1)
print lib.int_val(lib.field(l, 0))
l = lib.field(l, 1)
print lib.int_val(lib.field(l, 0))

TODO