pybind11: C++ 工程如何提供 Python 介面

GoCodingInMyWay發表於2020-09-04

C/C++ 工程提供 Python 介面,有利於融合進 Python 的生態。現在 Python 在應用層,有其得天獨厚的優勢。尤其因為人工智慧和大資料的推波助瀾, Python 現在以及未來,將長期是最流行的語言之一。

那 C/C++ 怎麼提供 Python 介面呢?

  1. ctypes: C 與 Python 繫結, Python 內建模組
  2. Boost.Python: C++ 與 Python 繫結, Boost 模組
  3. pybind11: C++11 與 Python 繫結, 減去了舊 C++ 支援,更輕量化

本文將介紹 pybind11 的環境準備與入門使用。

環境準備

pybind11 是一個 header-only 的庫,換句話說,只需要 C++ 專案裡直接 include pybind11 的標頭檔案就能使用。

這裡則介紹如何於 CMake 裡引入 pybind11 。而更多編譯系統的介紹,可見官方文件 Build systems

獲取 pybind11

可以 git submodule 新增子模組,最好固定為某個版本:

git submodule add https://github.com/pybind/pybind11.git third_party/pybind11-2.5.0
cd third_party/pybind11-2.5.0/
git checkout tags/v2.5.0

或者,直接獲取原始碼,放進相應子目錄即可。

新增進 CMake

CMakeLists.txtadd_subdirectory pybind11 的路徑,再用其提供的 pybind11_add_module 就能建立 pybind11 的模組了。

cmake_minimum_required(VERSION 3.1)
project(start-pybind11 VERSION 0.1.0 LANGUAGES C CXX)

set(MY_PYBIND ${MY_CURR}/third_party/pybind11-2.5.0)

add_subdirectory(${MY_PYBIND})
pybind11_add_module(example_pb example_pb.cpp)

如果想在已有 C++ 動態庫上擴充套件 pybind11 繫結,那麼 target_link_libraries 連結該動態庫就可以了。

target_link_libraries(example_pb PUBLIC example)

繫結一個函式

我們先實現一個 add 函式,

int add(int i, int j) {
  return i + j;
}

為了簡化工程,可以直接實現在 example_pb.cpp 裡,

#include <pybind11/pybind11.h>

namespace py = pybind11;

int add(int i, int j) {
  return i + j;
}

PYBIND11_MODULE(example_pb, m) {
  m.doc() = "example_pb bindings";

  m.def("add", &add, "A function which adds two numbers");
}

之後,於 CMakeLists.txt 所在目錄,執行 cmake 編譯就完成了。

示例程式碼

繫結一個類

我們先實現一個定時觸發器的類。使用如下:

#include <iostream>

#include "tick.h"

int main(int argc, char const *argv[]) {
  (void)argc;
  (void)argv;

  Tick tick(500, 5000);

  tick.SetTickEvent([&tick](std::int64_t elapsed_ms) {
    std::cout << "elapsed: " << elapsed_ms << " ms" << std::endl;
    if (elapsed_ms >= 2000) {
      tick.Stop();
    }
  });

  tick.Start();
  tick.WaitLifeOver();
  return 0;
}

執行結果:

$ ./_output/bin/cpp_thread_callback/tick_test
elapsed: 0 ms
elapsed: 500 ms
elapsed: 1000 ms
elapsed: 1500 ms
elapsed: 2000 ms

該類的宣告如下:

using TickEvent = std::function<void(std::int64_t elapsed_ms)>;
using TickRunCallback = std::function<void()>;

class Tick {
 public:
  using clock = std::chrono::high_resolution_clock;

  Tick(std::int64_t tick_ms,
       std::int64_t life_ms = std::numeric_limits<std::int64_t>::max());
  Tick(TickEvent tick_event, std::int64_t tick_ms,
       std::int64_t life_ms = std::numeric_limits<std::int64_t>::max(),
       TickRunCallback run_beg = nullptr,
       TickRunCallback run_end = nullptr);
  virtual ~Tick();

  bool IsRunning() const;

  void Start();
  void Stop(bool wait_life_over = false);

  const std::chrono::time_point<clock> &GetTimeStart() const;

  void SetTickEvent(TickEvent &&tick_event);
  void SetTickEvent(const TickEvent &tick_event);

  void SetRunBegCallback(TickRunCallback &&run_beg);
  void SetRunBegCallback(const TickRunCallback &run_beg);

  void SetRunEndCallback(TickRunCallback &&run_end);
  void SetRunEndCallback(const TickRunCallback &run_end);

  void WaitLifeOver();

 protected:
  // ...
};

然後, pybind11 繫結實現如下:

#include <pybind11/pybind11.h>
#include <pybind11/chrono.h>
#include <pybind11/functional.h>

#include <memory>

#include "cpp/cpp_thread_callback/tick.h"

namespace py = pybind11;
using namespace pybind11::literals;  // NOLINT

PYBIND11_MODULE(tick_pb, m) {
  m.doc() = "tick_pb bindings";

  py::class_<Tick, std::shared_ptr<Tick>>(m, "Tick")
    .def(py::init<std::int64_t, std::int64_t>())
    .def(py::init<TickEvent, std::int64_t, std::int64_t,
                  TickRunCallback, TickRunCallback>())
    .def_property_readonly("is_running", &Tick::IsRunning)
    .def("start", &Tick::Start)
    .def("stop", &Tick::Stop, "wait_life_over"_a = false)
    .def("get_time_start", &Tick::GetTimeStart)
    .def("set_tick_event", [](Tick &self, const TickEvent &tick_event) {
      self.SetTickEvent(tick_event);
    })
    .def("set_run_beg_callback", [](Tick &self,
        const TickRunCallback &run_beg) {
      self.SetRunBegCallback(run_beg);
    })
    .def("set_run_end_callback", [](Tick &self,
        const TickRunCallback &run_end) {
      self.SetRunEndCallback(run_end);
    })
    .def("wait_life_over", &Tick::WaitLifeOver,
        py::call_guard<py::gil_scoped_release>());
}

編譯出動態庫後,把路徑新增進 PYTHONPATH

export PYTHONPATH=<path>:$PYTHONPATH

# 依賴其他動態庫的話,把路徑新增進 LIBRARY_PATH
# Linux
export LD_LIBRARY_PATH=<path>:$LD_LIBRARY_PATH
# macOS
export DYLD_LIBRARY_PATH=<path>:$DYLD_LIBRARY_PATH

之後,就可以於 Python 裡呼叫了:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=missing-docstring, import-error
import tick_pb as tick


def _main():
  t = tick.Tick(lambda elapsed_ms: print(f"elapsed: {elapsed_ms} ms"),
                500, 1000,
                lambda: print("run beg"), lambda: print("run end"))
  t.start()
  t.wait_life_over()


if __name__ == "__main__":
  _main()

執行結果:

$ python src/pybind/cpp_thread_callback/tick_test.py
run beg
elapsed: 0 ms
elapsed: 500 ms
elapsed: 1000 ms
run end

示例程式碼

執行示例程式碼

獲取程式碼,

git clone https://github.com/ikuokuo/start-pybind11.git

# 獲取子模組
cd start-pybind11/
git submodule update --init

編譯安裝,

# 依賴 cmake

cd start-pybind11/
make install

編譯結果,

$ tree _install
_install
├── bin
│   └── cpp_thread_callback
│       └── tick_test
└── lib
    ├── cpp_thread_callback
    │   ├── libtick.0.1.0.dylib
    │   ├── libtick.0.1.dylib -> libtick.0.1.0.dylib
    │   ├── libtick.dylib -> libtick.0.1.dylib
    │   ├── tick_pb.0.1.0.cpython-37m-darwin.so
    │   ├── tick_pb.0.1.cpython-37m-darwin.so -> tick_pb.0.1.0.cpython-37m-darwin.so
    │   └── tick_pb.cpython-37m-darwin.so -> tick_pb.0.1.cpython-37m-darwin.so
    └── first_steps
        ├── first_steps_pb.0.1.0.cpython-37m-darwin.so
        ├── first_steps_pb.0.1.cpython-37m-darwin.so -> first_steps_pb.0.1.0.cpython-37m-darwin.so
        ├── first_steps_pb.cpython-37m-darwin.so -> first_steps_pb.0.1.cpython-37m-darwin.so
        ├── libfirst_steps.0.1.0.dylib
        ├── libfirst_steps.0.1.dylib -> libfirst_steps.0.1.0.dylib
        └── libfirst_steps.dylib -> libfirst_steps.0.1.dylib

5 directories, 13 files

新增路徑,

$ source setup.bash first_steps cpp_thread_callback
DYLD_LIBRARY_PATH, PYTHONPATH
+ /Users/John/Workspace/Self/ikuokuo/start-pybind11/_install/lib/first_steps
+ /Users/John/Workspace/Self/ikuokuo/start-pybind11/_install/lib/cpp_thread_callback

執行示例,

$ python src/pybind/cpp_thread_callback/tick_test.py
run beg
elapsed: 0 ms
elapsed: 500 ms
elapsed: 1000 ms
run end

結語

Go coding!

相關文章