Pythonからlibtccを使って C言語ソースをJIT実行

<<追記>>
いろいろ試した結果、ひとつの文をコンパイルし終わるたびにtcc_deleteを呼び出して、一回 ライブラリをリセットしてやらないといけないみたい。

今回のコードだと使いにくいので、あとで書き直しますー


この記事の続きです
tccのインストール

今回はPythonからlibtccを呼び出し、C言語のソースプログラムをネイティブの機械語にコンパイルして実行するためのラッパークラスを書きます。(win32用)

こちらのモジュールを参考にしました。
Cinpy – C in Python

方法としては二つ考えられまして

-PythonからC言語の関数に文字列を渡して実行。
主な処理はC言語で書く (こっちも書きました)
-Python側でlibtccの関数を実行する

今回は、2番目の方法にしました。
そのほうが、クラスなども使えますし、多分、細かいところを改良しやすいんじゃなかろうか、という読みです。


tccでC言語ソースプログラムをコンパイルしてC関数を取得する場合、ヒープ操作(malloc)が必要になります。
まずは、tccのライブラリのmallocをPythonから使用できるようにします。

以下を mem.c として保存。
<<少し修正しました>>

//+---------------------------------------------------------------------------
//
//  mem.c - memory manipulate function for DLL
//
#include <stdio.h>
#include <stdlib.h>

void*	_calloc(size_t x, size_t y){ return calloc(x,y); }
void*	_malloc(size_t x){ return malloc( x );}
void*	_realloc(void* x, size_t y){ return realloc(x,y ); }
void	_free(void* x){ return free(x);}

コマンドラインにて、tccでDLLを作成。

tcc -shared -rdynamic mem.c

mem.dllが出来ているはず。
これはPythonプログラムから使用します。


以下を libtcc.py として保存

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

import ctypes,os

LIB_FLAG=True

class Tcc(object):
    _libtcc=None
    _libmem=None #tccのmalloc関数

    def __init__(self,libtcc="libtcc.dll",mem="mem.dll"):
        self.memlist=[]
        if not Tcc._libtcc:
            Tcc._libtcc=ctypes.cdll.LoadLibrary(libtcc)
        if not Tcc._libmem:
            Tcc._libmem=ctypes.cdll.LoadLibrary(mem)
        _libtcc=Tcc._libtcc
        stat=self.tccstate=_libtcc.tcc_new()
        _libtcc.tcc_set_output_type(stat,0)

        #カレントフォルダ、モジュールの置かれたフォルダを
        #ライブラリパスに追加
        
        if LIB_FLAG:        
        #add libpath
            _libtcc.tcc_add_library_path(stat,os.getcwd())
            if __name__!="__main__":
                _libtcc.tcc_add_library_path(os.path.split(__file__)[0])
    #ライブラリ関連関数は まだ 未テスト。 あとで調べます
    def add_library_path(self,library_path):
        u"-L オプション相当。ライブラリ検索パスを追加"
        return self._libtcc.tcc_add_library_path(self.tccstate,library_path)
        
    def add_library_path(self,library_name):
        u"-l オプション相当。 使用する外部ライブラリを追加"
        return self._libtcc.tcc_add_library(self.tccstate,library_name)
           
    def eval(self,func_name,cfunctype,csrc):
        lib=self._libtcc
        stat=self.tccstate
        lib.tcc_compile_string(stat,csrc)
        memsz=lib.tcc_relocate(stat,None)
        if memsz :
            mem=self._libmem._malloc(memsz)
            self.memlist.append(mem)
            lib.tcc_relocate(stat,mem)
            retp=lib.tcc_get_symbol(stat,func_name)
            return cfunctype(retp)
        
    def __del__(self):
        self._libtcc.tcc_delete(self.tccstate)
        #方針転換のためコメントアウト
        #メモリを開放しなければ、JITコンパイラを終了しても
        #引き続きC関数が使えるため。
        #C関数のメモリはPythonインタプリタ終了時に開放される(はず)
        #Python実行中に、mallocしたメモリを開放するには、別の機構が必要になります 
        #メモリクリーナー・クラス、とか、、、     
        if False :
            for mem in self.memlist:
                try:
                    self._libmem._free(mem)
                except:
                    pass
#test

if __name__=="__main__":

    csrc="""
int fib(n)
{
	if (n <= 2)
		return 1;
	else
		return fib(n-1) + fib(n-2);
}
"""
    
    tcc=Tcc()
    rettype=ctypes.c_long #C関数 戻り値の型
    argtypes=[ctypes.c_long] #C関数 引数の型リスト
    cprototype=ctypes.CFUNCTYPE(rettype,*argtypes) #関数プロトタイプ宣言
    
    f=tcc.eval("fib",cprototype,csrc)
    tcc=None #コンパイラ終了
    result=[1,1, 1, 2, 3, 5,8,13,21,34,55,89,144]
    for i,r in enumerate(result):
        ret=f(i)
        #print ret
        assert ret==r

    print "done"

その後調べた結果、ひとつの文をコンパイルするたびに

tcc=None

を実行してやらないといけないことが判明。あまりクラスにする意味がなかったかも。。。


PythonからC言語ソースをJITコンパイラで実行できると、ものによっては、かなり高速化できるのでは、と期待。

今後は、Blenderなどの組み込み環境でも動くのか、などなど、すこしずつ時間のあるときに調査したいと思います。

コメントを残す

メールアドレスが公開されることはありません。