最近ある事情から様々な言語ランタイムについて調べているのですが、luaやcpython、quickjsなどを調べている中でpocketpyというpythonランタイムの存在を知りました。
cpythonと比べると機能は少ないですが、パフォーマンスがcpythonより良いみたいです。

以下githubリポジトリ

GitHub - pocketpy/pocketpy: Portable Python 3.x Interpreter in Modern C for Game Scripting
Portable Python 3.x Interpreter in Modern C for Game Scripting - pocketpy/pocketpy

面白そうなので実際に動かしてみます。

環境

  • 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_r0py_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なんかが使えそうです。

GitHub - microsoft/debugpy: An implementation of the Debug Adapter Protocol for Python
An implementation of the Debug Adapter Protocol for Python - microsoft/debugpy

vscode拡張でも利用できるみたいなので、cpythonであればデバッグも容易ですね。
pocketpyはファイル数が少ないのは魅力ですが、デバッグの手間を考えると実行速度のアドを捨ててでもcpythonを使うのもありでしょうか。

小さめの自作ゲームエンジンとかで使い所はあると思います。