FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Pythonを経由してC++のオブジェクトを扱う方法(2. C++のオブジェクトをPythonのオブジェクトのように扱う方法)

f:id:msyksphinz:20181031233537p:plain

自作RISC-Vシミュレータは、バイナリファイルを指定するとそれを読み込んで、指定したプログラムカウンタの場所からシミュレーションを実行し、tohostのアクセスに到達するか最大実行サイクルに到達すると終了するのだが、そうでなく、もう少しInteractiveに操作できるようになるとうれしい。

前回は中途半端な状態でC++のシミュレータをPythonで動くようにした。ただし、最終目標は以下のように、 シミュレーションターゲットをオブジェクトのように扱うことだ。

#本当はchip.simulate()のように記述したいのだけれども、まだその方法が良く分からない。。。

import riscv_sim as riscv
chip = make_chip()
riscv.set_pc(chip, 0x0)
riscv.load_hex(chip, "test.riscv")
riscv.debug_mode(chip, true)
riscv.simulate(chip, 1000)

これを実現するための方法について調査していたのだが、やっとその方法を見つけて実装ができたのでそれをまとめておく。

参考にしたのは以下だ。

2. 拡張の型の定義: チュートリアル — Python 3.7.1 ドキュメント

実装したのは以下。python3_env.cppに実装している。

github.com

拡張型の定義

今回オブジェクトとしてPython上で扱いたいのは、シミュレータの実体(ここではRiscvPeThreadというクラスで定義されている、RISC-Vのアーキテクチャとシミュレーションエンジンを含んだクラス)だ。

これをPythonで扱うために、Python用のオブジェクトでWrapする。ここではRiscvPeObjectとした。

typedef struct {
  PyObject_HEAD
  RiscvPeThread *pe_thread;
} RiscvPeObject;

このオブジェクトに対する変数と、メソッドを定義するためにPyTypeObject構造体を新しい型を定義する。

static PyTypeObject RiscvPeType = {
  PyVarObject_HEAD_INIT(NULL, 0)
  tp_name      : "riscv.RiscvPe",
  tp_basicsize : (Py_ssize_t) sizeof(RiscvPeObject),
  tp_itemsize  : 0,
  tp_dealloc   : (destructor) RiscvPeDealloc,
  tp_flags     : Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
  tp_doc       : "Riscv objects",
  tp_methods   : riscv_chip_methods,
  tp_members   : NULL,
  tp_init      : (initproc) InitRiscvChip,
  tp_new       : MakeRiscvChip
};

ここで設定したのは、

  • tp_name : オブジェクトの型名
  • tp_basicsize : オブジェクトのサイズ
  • tp_methods : オブジェクトから呼び出すことのできるメソッド。ここではriscv_chip_methods変数でメソッド一覧を定義した。
  • tp_members : オブジェクトの参照できる変数。ここではNULLだが、変数を経由して参照できる変数一覧を定義できる。
  • tp_init : オブジェクトをインスタンスしたときに呼ばれるinit関数を定義する。
  • tp_new : オブジェクトをnewしたときに呼ばれる関数を定義する。

riscv_chip_methodsでこのオブジェクトに割り付けられているメソッドを定義した。 ここでは、py_add(テスト用)、simulateload_binの3つのメソッドを定義している。

PyMethodDef riscv_chip_methods[] = {
  { "py_add"     , (PyCFunction)HelloAdd            , METH_VARARGS, "Example ADD"                    },
  { "simulate"   , (PyCFunction)SimRiscvChip        , METH_VARARGS, "Simulate RiscvChip"             },
  { "load_bin"   , (PyCFunction)LoadBinaryRiscvChip , METH_VARARGS, "Load Binary file"               },
  { NULL         , NULL                             ,            0, NULL                             } /* Sentinel */
};

simulate()が呼び出されると、C++側ではSimRiscvChip()が、load_bin()が実行されるとLoadBinaryRiscvChip ()が呼び出されるようになっている。

それぞれは以下のように定義している。

static PyObject* SimRiscvChip (RiscvPeObject *self, PyObject* args)
{
  self->pe_thread->SetMaxCycle (100);
  self->pe_thread->StepSimulation(100, LoopType_t::FiniteLoop);

  return PyLong_FromLong(0);
}

static PyObject* LoadBinaryRiscvChip (RiscvPeObject *self, PyObject* args)
{
  char *filename;
  if (!PyArg_ParseTuple(args, "s", &filename)) {
    return PyLong_FromLong (-1);
  }

  if (self->pe_thread->LoadBinary("swimmer_riscv", filename, true) == -1) {
    return PyLong_FromLong (-1);
  }

  return PyLong_FromLong (0);
}

さらに、tp_newで設定したnew時に呼ばれる関数として MakeRiscvChip()を実装した。

static PyObject* MakeRiscvChip (PyTypeObject *type, PyObject* args, PyObject *kwds)
{
  RiscvPeObject *self = (RiscvPeObject *) type->tp_alloc(type, 0);

  RiscvPeThread *chip = new RiscvPeThread (stdout, RiscvBitMode_t::Bit64, 0xffffffff, PrivMode::PrivUser,
                                           true, true, stdout, true, "trace_out.log");

  self->pe_thread = chip;

  return (PyObject *)self;
}

これでビルドをして、Pythonモードを呼び出してみる。 以下のコマンドでシミュレーションができるようになった。これはうれしい。

$ swimmer_riscv --py
>>> chip = riscv.RiscvChip()
>>> chip.load_bin("test.elf")
>>> chip.simulate(1000)