pocketpyを埋め込む
最近ある事情から様々な言語ランタイムについて調べているのですが、luaやcpython、quickjsなどを調べている中でpocketpy
というpythonランタイムの存在を知りました。
cpythonと比べると機能は少ないですが、パフォーマンスがcpythonより良いみたいです。
以下githubリポジトリ
面白そうなので実際に動かしてみます。
環境
- macos 15.3.2 M4
- cmake 3.31.6
- pocketpy 2.0.6
Hello Worldしてみる
pocketpy、必要なファイルが以下の二つしかないのが素晴らしい(中身はめちゃくちゃデカいですが)
- pocketpy.c
- pocketpy.h
どちらもReleaseページからダウンロード可能です。
以下のようなmain.cppとCMakeLists.txtを作成してビルド・実行しました。
cmake_minimum_required(VERSION 3.31)
project(app LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(pkpy STATIC pocketpy.c)
target_compile_features(pkpy PUBLIC c_std_11)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE pkpy)
#include <iostream>
#include "pocketpy.h"
int main() {
py_initialize();
bool ok = py_exec("print('Hello World!')", "<string>", EXEC_MODE, nullptr);
if(ok == false) {
std::cout << "error!" << std::endl;
py_finalize();
return 1;
}
py_finalize();
return 0;
}
cmake -S . -B build
cmake --build build
./build/main
Hello World!
py_initialize
でvmの初期化を行い、py_exec
でステートメントを実行し、終了処理としてpy_finalize
を呼び出しています。
簡単ですね。
c,c++関数をバインドする
c側の関数をpythonコードから呼び出すことも可能。
c側でadd
関数とpy_add
関数を定義し、py_bindfunc
を利用してpythonから呼び出せるようにしている。
py_add関数内ではpythonコードからの引数の数などをPY_CHECK_*
マクロで検証している。
#include <iostream>
#include "pocketpy.h"
int add(int a, int b) { return a + b; }
bool py_add(int argc, py_Ref argv) {
PY_CHECK_ARGC(2);
PY_CHECK_ARG_TYPE(0, tp_int);
PY_CHECK_ARG_TYPE(1, tp_int);
int _0 = py_toint(py_arg(0));
int _1 = py_toint(py_arg(1));
int res = add(_0, _1);
py_newint(py_retval(), res);
return true;
}
int main() {
py_initialize();
// モジュールに関数を追加、バインド
py_GlobalRef mod = py_getmodule("__main__");
py_bindfunc(mod, "add", py_add);
bool ok = py_exec("print(add(100, 200))", "<string>", EXEC_MODE, nullptr);
if(ok == false) {
std::cout << "error!" << std::endl;
py_finalize();
return 1;
}
py_finalize();
return 0;
}
300
py_arg
が一見関数のように見えますが、マクロで以下のように定義されています。
#define py_arg(i) py_offset(argv, i)
このことからargcやargvなどの引数の名称が固定なのがわかります。
レジスタ・スタックを操作する
c側からpythonランタイムのレジスタ・スタックを操作することができます。
レジスタはpy_getreg
関数やpy_r0
〜 py_r7
マクロで参照を得られる。
また、py_retval
関数で戻り値がセットされているレジスタの参照を得られる。
pocketpyではレジスタは8個用意されていて、それとは別に戻り値を返すためのレジスタが1つあるようです。
以下はレジスタを利用した足し算を行うプログラムです。
#include <iostream>
#include "pocketpy.h"
int main() {
py_initialize();
py_Ref r0 = py_r0();
py_Ref r1 = py_r1();
// レジスタに値をセット
py_newint(r0, 1000);
py_newint(r1, 2000);
// 足し算マクロ 中身はpy_binaryop関数
py_binaryadd(r0, r1);
// レジスタから結果を受け取る
int result = py_toint(py_retval());
std::cout << result << std::endl;
py_finalize();
return 0;
}
3000
以下のようにスタックを利用することで関数に引数を与えることができる。
#include <iostream>
#include "pocketpy.h"
int main() {
py_initialize();
py_GlobalRef r0 = py_r0();
// print関数取得
py_ItemRef f_print = py_getbuiltin(py_name("print"));
// 文字列をレジスタにセット
py_newstr(r0, "sample value");
// 関数,nil,引数をスタックにプッシュ
py_push(f_print);
py_pushnil();
py_push(r0);
// 関数呼び出し
// 引数1:selfを除いた引数の数
// 引数2:キーワード付き引数の数
py_vectorcall(1, 0);
py_finalize();
return 0;
}
sample value
レジスタ・スタックを活用してクラスメソッドの呼び出しもできる。
#include <iostream>
#include "pocketpy.h"
const char* class_script = R"EOF(
class Sample:
def __init__(self, value1, value2):
self.value1 = value1
self.value2 = value2
def method(self):
print(self.value1 + self.value2)
)EOF";
int main() {
py_initialize();
py_exec(class_script, "<string>", EXEC_MODE, nullptr);
// クラスの情報を取得する
py_Type class_def = py_gettype("__main__", py_name("Sample"));
// インスタンスを生成する
py_Ref argv[2];
argv[0] = py_r0();
argv[1] = py_r1();
py_newint(argv[0], 200);
py_newint(argv[1], 300);
bool ok = py_tpcall(class_def, 2, *argv);
if(!ok) {
std::cout << "instantiate error" << std::endl;
py_printexc();
py_finalize();
return 1;
}
py_Ref instance = py_retval();
// メソッドを呼び出す
py_push(instance);
py_pushmethod(py_name("method"));
ok = py_vectorcall(0, 0);
if(!ok) {
std::cout << "call error" << std::endl;
py_printexc();
py_finalize();
return 1;
}
py_finalize();
return 0;
}
500
pythonのクラスインスタンスのメソッドをc++から呼び出す
多くの人がやりたいのは、バッチ処理をpythonで記述したい or させたいとか、クラスに実装したライフサイクルメソッドの呼び出し等(ゲームエンジンなどでよくあるGameObject.Update
とかNode._process
みたいなやつ)ではないでしょうか。
以下ではpythonクラスにon_init、update、on_destroyの3つのメソッドを実装し、c側でインスタンス作成・各ライフサイクルメソッドを呼び出してみます。
#include <iostream>
#include "pocketpy.h"
const char* class_script = R"EOF(
class GameObject :
def on_init(self):
print('init')
def update(self):
print('update')
def on_destroy(self):
print('destroy')
)EOF";
int main() {
py_initialize();
py_exec(class_script, "<string>", EXEC_MODE, nullptr);
py_Name gameobject_varname = py_name("gameObject");
py_Type class_def = py_gettype("__main__", py_name("GameObject"));
py_tpcall(class_def, 0, nullptr);
py_Ref instance = py_retval();
py_setglobal(gameobject_varname, instance);
instance = py_getglobal(gameobject_varname);
py_push(instance);
py_pushmethod(py_name("on_init"));
py_vectorcall(0, 0);
// とりあえず10回updateを呼び出す
int i = 0;
py_Name update_name = py_name("update");
while(i < 10) {
i++;
py_push(instance);
py_pushmethod(update_name);
py_vectorcall(0, 0);
}
py_push(instance);
py_pushmethod(py_name("on_destroy"));
py_vectorcall(0, 0);
py_setglobal(gameobject_varname, py_NIL());
py_finalize();
return 0;
}
init
update
update
update
update
update
update
update
update
update
update
destroy
注意しなければいけないのが、py_tpcall
で作成したインスタンスの参照をpython内で保持しておかないと、インスタンスがGCで解放されてしまってメソッドを呼び出せなくなります。
このコードではpy_setglobal
関数を利用して参照を維持しています。
GCで解放されてしまっているかはpy_isnone
マクロかpy_istype
関数でチェックできる。py_isnoneは内部的にpy_istypeを呼び出しているだけです。
デバッグ
breakpoint()という関数が用意されていてそれを呼び出すことで、対話型のデバッグができるみたいです。
しかし、現在v2系統では利用できないようです。
感想
ファイルが少なくて簡単に利用できてcpythonより高速なので、もし自分のアプリにpythonランタイムを埋め込むとしたらpocketpy一択かなぁ・・・と言いたいところですが、pocketpyはデバッグ機能が貧弱です。
breakpoint()は今後v2でも動作するようになるでしょうが、IDE等のデバッガーを接続する機能がありません。
cpythonであればdebugpyなんかが使えそうです。
vscode拡張でも利用できるみたいなので、cpythonであればデバッグも容易ですね。
pocketpyはファイル数が少ないのは魅力ですが、デバッグの手間を考えると実行速度のアドを捨ててでもcpythonを使うのもありでしょうか。
小さめの自作ゲームエンジンとかで使い所はあると思います。