PyPy Advent Calendar 15日目 - 低レベルっぽいことをやってみる

PyPy Advent Calendar 15日目を担当する二周目の[twitter:@shomah4a]です。だんだんとネタもなくなってきました。
でもね、 rlib 探すと色々面白いものがあるんだね。

というわけで今回はなんとなく目についた pypy.rlib.clibffi についてです。

※まとめのあとに追記しました
※libm っておかしくね? って言われたので修正。動いていた理由は不明

ffi ってなによ?

ffi とは、Foreign Function Interface と呼ばれるもので、日本語に訳すと他言語関数インタフェイスというらしいです。

要は別の言語の関数を呼び出すための仕組みです。

clibffi の場合は Python から C の関数を呼ぶものです。

そもそも C の関数を呼べないことにはソケット通信もできませんし、スレッドも立てられません。
この clibffi を使うことで ctypes やスレッド、ソケットなどのライブラリ実装したりしているんですね。多分。

早速使ってみる

簡単な C 関数の例として putchar を呼んでみます。

putchar の定義は以下のようなものです。

int putchar(int c);

const が足りませんね! ではなくて簡単ですね。

putchar は文字を一文字標準出力に出力する関数です。

で、これを呼ぶには以下のようにします。

from pypy.rlib import clibffi
from pypy.rpython.lltypesystem import rffi

lib = clibffi.CDLL(clibffi.get_libc_name())

func = lib.getpointer('putchar',
                      [clibffi.ffi_type_sint],
                      clibffi.ffi_type_sint)

func.push_arg(ord('a'))
    
result = func.call(rffi.INT)

print func
print 'result:', result

割と自明ではあるのですが、一行ずつ解説します。

lib = clibffi.CDLL(clibffi.get_libc_name())

この行は libc というライブラリを読み込んでいます。
環境によってそれっぽいライブラリ名をとってきてくれる get_libc_name を使っています。

func = lib.getpointer('putchar',
                      [clibffi.ffi_type_sint],
                      clibffi.ffi_type_sint)

この行では、読み込んだ libc に定義されている putchar 関数を取得しています。
第一引数は関数名、第二引数は引数の型のリスト、第三引数は返値の型を表します。

この例では符号付き整数を受け取り、符号付き整数を返す関数です。

func.push_arg(ord('a'))

で、この取得したオブジェクトに対して引数を渡してあげ、

result = func.call(rffi.INT)

ここで呼び出します。
返値の型なのか受け取る型なのかわかりませんが、渡してあげないといけないようです。

これを呼び出すと標準出力に a が出力されます。
result には putchar の返り値である 97 ('a' の値) が入ります。

たったこれだけですが結構めんどくさいですね。

これを諸々 Wrap したものが ctypes なのでしょう。

実行してみる

このソースコードはそのまま Python で実行できます。

$ export PYTHONPATH=/path/to/pypy
$ python ffitest.py
[libffi:WARNING] 'libffi.a' not found in []
[libffi:WARNING] trying to use the dynamic library instead...
[platform:execute] gcc -c -O3 -pthread -fomit-frame-pointer -Wall -Wno-unused /tmp/usession-default-55/platcheck_5.c -o /tmp/usession-default-55/platcheck_5.o
[platform:execute] gcc /tmp/usession-default-55/platcheck_5.o -pthread -lffi -lrt -o /tmp/usession-default-55/platcheck_5
[platform:execute] gcc -c -O3 -pthread -fomit-frame-pointer -Wall -Wno-unused /tmp/usession-default-55/platcheck_6.c -o /tmp/usession-default-55/platcheck_6.o
[platform:execute] gcc /tmp/usession-default-55/platcheck_6.o -pthread -lffi -lrt -o /tmp/usession-default-55/platcheck_6
a<pypy.rlib.clibffi.FuncPtr object at 0x1856c50>
result: 97

実行できました。
はい。
puchar は一文字しか出力しないので、その後の "print func" の出力の前に埋れていますが 'a' が出力されていますね。

translate.py

さらにここまで来たら折角なので translate.py してみましょう。

$ export PYTHONPATH=/path/to/pypy
$ python /path/to/pypy/pypy/translator/goal/translate.py ffitest.py

.. 略 ..

[Timer] ========================================
[Timer] Total:                         --- 1.5 s
[translation:ERROR] Error:
[translation:ERROR]  Traceback (most recent call last):
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/translator/goal/translate.py", line 308, in main
[translation:ERROR]     drv.proceed(goals)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/translator/driver.py", line 809, in proceed
[translation:ERROR]     return self._execute(goals, task_skip = self._maybe_skip())
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/translator/tool/taskengine.py", line 116, in _execute
[translation:ERROR]     res = self._do(goal, taskcallable, *args, **kwds)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/translator/driver.py", line 286, in _do
[translation:ERROR]     res = func()
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/translator/driver.py", line 323, in task_annotate
[translation:ERROR]     s = annotator.build_types(self.entry_point, self.inputtypes)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/annrpython.py", line 103, in build_types
[translation:ERROR]     return self.build_graph_types(flowgraph, inputcells, complete_now=complete_now)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/annrpython.py", line 194, in build_graph_types
[translation:ERROR]     self.complete()
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/annrpython.py", line 250, in complete
[translation:ERROR]     self.processblock(graph, block)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/annrpython.py", line 448, in processblock
[translation:ERROR]     self.flowin(graph, block)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/annrpython.py", line 508, in flowin
[translation:ERROR]     self.consider_op(block.operations[i])
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/annrpython.py", line 710, in consider_op
[translation:ERROR]     raise_nicer_exception(op, str(graph))
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/annrpython.py", line 707, in consider_op
[translation:ERROR]     resultcell = consider_meth(*argcells)
[translation:ERROR]    File "<134-codegen /home/shoma/files/pypy/pypy/annotation/annrpython.py:745>", line 3, in consider_op_simple_call
[translation:ERROR]     return arg.simple_call(*args)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/unaryop.py", line 175, in simple_call
[translation:ERROR]     return obj.call(getbookkeeper().build_args("simple_call", args_s))
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/unaryop.py", line 696, in call
[translation:ERROR]     return bookkeeper.pbc_call(pbc, args)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/bookkeeper.py", line 667, in pbc_call
[translation:ERROR]     results.append(desc.pycall(schedule, args, s_previous_result, op))
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/description.py", line 283, in pycall
[translation:ERROR]     result = self.specialize(inputcells, op)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/description.py", line 279, in specialize
[translation:ERROR]     return self.specializer(self, inputcells)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/specialize.py", line 80, in default_specialize
[translation:ERROR]     graph = funcdesc.cachedgraph(key, builder=builder)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/description.py", line 237, in cachedgraph
[translation:ERROR]     graph = self.buildgraph(alt_name, builder)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/annotation/description.py", line 200, in buildgraph
[translation:ERROR]     graph = translator.buildflowgraph(self.pyobj)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/translator/translator.py", line 77, in buildflowgraph
[translation:ERROR]     graph = space.build_flow(func)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/objspace/flow/objspace.py", line 279, in build_flow
[translation:ERROR]     ec.build_flow()
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/objspace/flow/flowcontext.py", line 264, in build_flow
[translation:ERROR]     self)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/pyopcode.py", line 85, in dispatch
[translation:ERROR]     next_instr = self.handle_bytecode(co_code, next_instr, ec)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/pyopcode.py", line 91, in handle_bytecode
[translation:ERROR]     next_instr = self.dispatch_bytecode(co_code, next_instr, ec)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/pyopcode.py", line 266, in dispatch_bytecode
[translation:ERROR]     res = meth(oparg, next_instr)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/pyopcode.py", line 695, in LOAD_GLOBAL
[translation:ERROR]     self.pushvalue(self._load_global(self.getname_u(nameindex)))
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/pyopcode.py", line 680, in _load_global
[translation:ERROR]     w_value = self.space.finditem_str(self.w_globals, varname)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/baseobjspace.py", line 714, in finditem_str
[translation:ERROR]     return self.finditem(w_obj, self.wrap(key))
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/baseobjspace.py", line 720, in finditem
[translation:ERROR]     if e.match(self, self.w_KeyError):
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/error.py", line 46, in match
[translation:ERROR]     return space.exception_match(self.w_type, w_check_class)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/objspace/flow/objspace.py", line 223, in exception_match
[translation:ERROR]     return ObjSpace.exception_match(self, w_exc_type, w_check_class)
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/baseobjspace.py", line 909, in exception_match
[translation:ERROR]     if self.is_w(w_exc_type, w_check_class):
[translation:ERROR]    File "/home/shoma/files/pypy/pypy/interpreter/baseobjspace.py", line 697, in is_w
[translation:ERROR]     return w_two.is_w(self, w_one)
[translation:ERROR]  AttributeError': 'Constant' object has no attribute 'is_w'
[translation:ERROR] 	.. v1 = simple_call((function dlsym), v0, name_0)
[translation:ERROR] 	.. '(pypy.rlib.clibffi:618)RawCDLL.getpointer'
[translation:ERROR] Processing block:
[translation:ERROR]  block@18 is a <class 'pypy.objspace.flow.flowcontext.SpamBlock'>
[translation:ERROR]  in (pypy.rlib.clibffi:618)RawCDLL.getpointer
[translation:ERROR]  containing the following operations:
[translation:ERROR]        v0 = getattr(self_0, ('lib'))
[translation:ERROR]        v1 = simple_call((function dlsym), v0, name_0)
[translation:ERROR]        v2 = call_args((type FuncPtr), ((4, ('flags', 'keepali... False)), name_0, argtypes_0, restype_0, v1, flags_0, self_0)
[translation:ERROR]  --end--
[translation] start debugger...
> /home/shoma/files/pypy/pypy/interpreter/baseobjspace.py(697)is_w()
-> return w_two.is_w(self, w_one)
(Pdb+) 

oh.. エラーでたよ…。

なんか色々頑張っても無理でした。

まとめ

多分これを使って色々実装してあるであろう pypy.rlib.rsocket あたりを見るといいのかなあと思いつつ見てみてもそんなことはなく、 pypy/module/_rawffi あたりに色々あるようで、ここらへんをフラフラしておくといいのかもしれないですね。

そもそも RPython の上からこのレイヤの事を色々やっちゃうと Backend の対応が大変そうだし、読み込んでいるモジュールを見る限りは lltypesystem まわりでゴニョゴニョやっているっぽいので、 translate.py の処理の中で使うことを想定しているものなのかもしれません。
lltype.malloc とか使っている感じだし、どう考えても RPython レイヤのライブラリではないですね。
というか clibffi で pypy のソースを find-grep しても大体そんな印象。

RPython より下のレイヤで色々やりたいぜ! って人は是非。

次は一周目と同じく[twitter:@iizukak]さんです。
よろしくお願いします。

追記

translate できなーいなんて言っていたら、 skype チャットで [twitter:@yanolab] さんが

[11時57分24秒 JST] Kenji Yano: print func
print 'result:', result
除けばtranslateできたようです。僕の環境では。

と言っていたので、 PyPy のリポジトリを hg pull -u して print 文を削除したらできました。
リポジトリ古いとか悲しすぎる…。

で、この print できない result オブジェクトは

>>> print result.__class__
<class 'pypy.rlib.rarithmetic.r_INT'>

というものであるらしく、 Python 上では動くのですが、 RPython 上では文脈から型が判別できないためなのか動かないようです。

これを動くようにするには以下のようにします。

>>> print int(result)
97

int でキャストしてあげることで型を明示して、動くようにしてあげるのです。

で、これを修正して translate.py 用の関数も追加したソースを以下に置いておきます。
これで C 関数を呼んだ返り値を受け取って処理するということができるようになりますね。

#-*- coding:utf-8 -*-
from pypy.rlib import clibffi
from pypy.rpython.lltypesystem import rffi


def main(args):

    lib = clibffi.CDLL(clibffi.get_libc_name())

    func = lib.getpointer('putchar',
                          [clibffi.ffi_type_sint],
                          clibffi.ffi_type_sint)

    func.push_arg(ord('a'))
    
    result = func.call(rffi.INT)

    print func

    print int(result)

    return 0


def target(*args, **argd):

    return main, None
    

if __name__ == '__main__':
    main([])

まとめの修正

まとめであんなふうに書いておいてアレなのですが、これが translate.py できるということは、オレオレ処理系の拡張はこれでガンガン書いちゃいなってことなのかもしれませんね。
なんにしても処理系を作る上で必要な知識がまたひとつ増えたようでよかったなあと言ったところでしょうか。
(そんな事考えるよりまずは PyPy の Python 処理系のソース見たほうがいいな…)