Hari ini saya ingin membahas topik mengintegrasikan Python ke dalam C ++.
Semuanya dimulai dengan panggilan dari seorang teman pada pukul dua pagi, yang mengeluh: "Kami memiliki produksi di bawah beban ..." Dalam percakapan, ternyata kode produksi ditulis menggunakan ipyparallel (paket Python yang memungkinkan komputasi paralel dan terdistribusi) untuk menghitung model dan mendapatkan hasil secara online. Kami memutuskan untuk memahami arsitektur ipyparallel dan melakukan profil di bawah beban.
Segera menjadi jelas bahwa semua modul dari paket ini dirancang dengan sempurna, tetapi sebagian besar waktu dihabiskan untuk jaringan, parsing json, dan tindakan perantara lainnya.
Setelah studi mendetail tentang ipyparallel, ternyata seluruh perpustakaan terdiri dari dua modul yang berinteraksi:
- Ipcontroler, yang bertanggung jawab untuk memantau dan menjadwalkan tugas,
- Engine, yang merupakan pelaksana kode.
Fitur yang bagus adalah modul ini berinteraksi melalui pyzmq. Berkat arsitektur mesin yang baik, kami berhasil mengganti implementasi jaringan dengan solusi kami yang dibangun di atas cppzmq. Penggantian ini membuka cakupan pengembangan tanpa akhir: mitra dapat ditulis di bagian C ++ aplikasi.
Ini membuat kumpulan mesin secara teoritis lebih cepat, tetapi masih tidak menyelesaikan masalah mengintegrasikan pustaka ke dalam kode Python. Jika Anda harus melakukan terlalu banyak hal untuk mengintegrasikan perpustakaan Anda, maka solusi seperti itu tidak akan dibutuhkan dan akan tetap ada di rak. Pertanyaannya tetap bagaimana mengintegrasikan perkembangan kami ke dalam basis kode mesin saat ini.
Kami membutuhkan beberapa kriteria yang masuk akal untuk memahami pendekatan mana yang harus diambil: kemudahan pengembangan, deklarasi API hanya di dalam C ++, tidak ada pembungkus tambahan di dalam Python, atau penggunaan asli dari kekuatan penuh perpustakaan. Dan agar tidak bingung dengan cara asli (dan tidak demikian) menyeret melalui kode C ++ dengan Python, kami melakukan sedikit riset. Di awal tahun 2019, empat cara populer untuk mengembangkan Python dapat ditemukan di Internet:
- Ctypes
- CFFI
- Cython
- API CPython
Kami telah mempertimbangkan semua opsi integrasi.
1. Jenis
Ctypes adalah Antarmuka Fungsi Asing yang memungkinkan Anda memuat pustaka dinamis yang mengekspor antarmuka C. Ini dapat digunakan untuk menggunakan pustaka C dari Python, misalnya, libev, libpq.
Misalnya, ada pustaka yang ditulis dalam C ++ dengan antarmuka:
extern "C"
{
Foo* Foo_new();
void Foo_bar(Foo* foo);
}
Kami menulis pembungkus untuk itu:
import ctypes
lib = ctypes.cdll.LoadLibrary('./libfoo.so')
class Foo:
def __init__(self) -> None:
super().__init__()
lib.Foo_new.argtypes = []
lib.Foo_new.restype = ctypes.c_void_p
lib.Foo_bar.argtypes = []
lib.Foo_bar.restype = ctypes.c_void_p
self.obj = lib.Foo_new()
def bar(self) -> None:
lib.Foo_bar(self.obj)
Kami menarik kesimpulan:
- Ketidakmampuan untuk berinteraksi dengan API interpreter. Ctypes adalah cara untuk berinteraksi dengan pustaka C di sisi Python, tetapi tidak menyediakan cara untuk kode C / C ++ untuk berinteraksi dengan Python.
- Mengekspor antarmuka C-style. Jenis dapat berinteraksi dengan pustaka ABI dalam gaya ini, tetapi bahasa lain apa pun harus mengekspor variabel, fungsi, metode melalui pembungkus C.
- Kebutuhan untuk menulis pembungkus. Keduanya harus ditulis di sisi kode C ++ untuk kompatibilitas ABI dengan C, dan di sisi Python untuk mengurangi jumlah kode boilerplate.
Jenis tidak cocok untuk kami, kami mencoba metode selanjutnya - CFFI.
2. CFFI
CFFI mirip dengan Ctypes tetapi memiliki beberapa fitur tambahan. Mari kita tunjukkan contoh dengan pustaka yang sama:
import cffi
ffi = cffi.FFI()
ffi.cdef("""
Foo* Foo_new();
void Foo_bar(Foo* foo);
""")
lib = ffi.dlopen("./libfoo.so")
class Foo:
def __init__(self) -> None:
super().__init__()
self.obj = lib.Foo_new()
def bar(self) -> None:
lib.Foo_bar(self.obj)
Kami menarik kesimpulan:
CFFI masih memiliki kelemahan yang sama, kecuali pembungkusnya menjadi sedikit lebih tebal, karena Anda perlu memberi tahu perpustakaan definisi antarmukanya. CFFI juga tidak cocok, mari kita lanjutkan ke metode selanjutnya - Cython.
3. Cython
Cython adalah bahasa pemrograman sub / meta yang memungkinkan Anda menulis ekstensi dalam campuran C / C ++ dan Python dan memuat hasilnya sebagai pustaka dinamis. Kali ini ada pustaka yang ditulis dalam C ++ dan memiliki antarmuka:
#ifndef RECTANGLE_H
#define RECTANGLE_H
namespace shapes {
class Rectangle {
public:
int x0, y0, x1, y1;
Rectangle();
Rectangle(int x0, int y0, int x1, int y1);
~Rectangle();
int getArea();
void getSize(int* width, int* height);
void move(int dx, int dy);
};
}
#endif
Kemudian kami mendefinisikan antarmuka ini dalam bahasa Cython:
cdef extern from "Rectangle.cpp":
pass
# Declare the class with cdef
cdef extern from "Rectangle.h" namespace "shapes":
cdef cppclass Rectangle:
Rectangle() except +
Rectangle(int, int, int, int) except +
int x0, y0, x1, y1
int getArea()
void getSize(int* width, int* height)
void move(int, int)
Dan kami menulis pembungkusnya:
# distutils: language = c++
from Rectangle cimport Rectangle
cdef class PyRectangle:
cdef Rectangle c_rect
def __cinit__(self, int x0, int y0, int x1, int y1):
self.c_rect = Rectangle(x0, y0, x1, y1)
def get_area(self):
return self.c_rect.getArea()
def get_size(self):
cdef int width, height
self.c_rect.getSize(&width, &height)
return width, height
def move(self, dx, dy):
self.c_rect.move(dx, dy)
# Attribute access
@property
def x0(self):
return self.c_rect.x0
@x0.setter
def x0(self, x0):
self.c_rect.x0 = x0
# Attribute access
@property
def x1(self):
return self.c_rect.x1
@x1.setter
def x1(self, x1):
self.c_rect.x1 = x1
# Attribute access
@property
def y0(self):
return self.c_rect.y0
@y0.setter
def y0(self, y0):
self.c_rect.y0 = y0
# Attribute access
@property
def y1(self):
return self.c_rect.y1
@y1.setter
def y1(self, y1):
self.c_rect.y1 = y1
Sekarang kita dapat menggunakan kelas ini dari kode Python biasa:
import rect
x0, y0, x1, y1 = 1, 2, 3, 4
rect_obj = rect.PyRectangle(x0, y0, x1, y1)
print(dir(rect_obj))
Kami menarik kesimpulan:
- Saat menggunakan Cython, Anda masih harus menulis kode pembungkus di sisi C ++, tetapi Anda tidak perlu lagi mengekspor antarmuka C-style.
- Anda masih tidak dapat berinteraksi dengan penerjemah.
Cara terakhir tetap - API CPython. Kami mencobanya.
4. API CPython
CPython API - API yang memungkinkan Anda mengembangkan modul untuk interpreter Python di C ++. Taruhan terbaik Anda adalah pybind11, pustaka C ++ tingkat tinggi yang membuat bekerja dengan API CPython menjadi nyaman. Dengan bantuannya, Anda dapat dengan mudah mengekspor fungsi, kelas, mengonversi data antara memori python dan memori asli di C ++.
Jadi, mari kita ambil kode dari contoh sebelumnya dan tulis pembungkusnya:
PYBIND11_MODULE(rect, m) {
py::class_<Rectangle>(m, "PyRectangle")
.def(py::init<>())
.def(py::init<int, int, int, int>())
.def("getArea", &Rectangle::getArea)
.def("getSize", [](Rectangle &rect) -> std::tuple<int, int> {
int width, height;
rect.getSize(&width, &height);
return std::make_tuple(width, height);
})
.def("move", &Rectangle::move)
.def_readwrite("x0", &Rectangle::x0)
.def_readwrite("x1", &Rectangle::x1)
.def_readwrite("y0", &Rectangle::y0)
.def_readwrite("y1", &Rectangle::y1);
}
Kami yang menulis bungkusnya, sekarang perlu dikompilasi ke dalam pustaka biner. Kami membutuhkan dua hal: sistem build dan manajer paket. Mari kita ambil CMake dan Conan untuk tujuan ini.
Agar build di Conan berfungsi, Anda perlu menginstal Conan itu sendiri dengan cara yang sesuai:
pip3 install conan cmake
dan daftarkan repositori tambahan:
conan remote add bincrafters https://api.bintray.com/conan/bincrafters/public-conan
conan remote add cyberduckninja https://api.bintray.com/conan/cyberduckninja/conan
Mari kita jelaskan dependensi proyek untuk pustaka pybind di file conanfile.txt:
[requires]
pybind11/2.3.0@conan/stable
[generators]
cmake
Mari tambahkan file CMake. Perhatikan integrasi yang disertakan dengan Conan - ketika CMake dijalankan, perintah conan install akan dijalankan, yang menginstal dependensi dan menghasilkan variabel CMake dengan informasi tentang dependensi:
cmake_minimum_required(VERSION 3.17)
set(project rectangle)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS OFF)
if (NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake")
message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan")
file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/v0.15/conan.cmake" "${CMAKE_BINARY_DIR}/conan.cmake")
endif ()
set(CONAN_SYSTEM_INCLUDES "On")
include(${CMAKE_BINARY_DIR}/conan.cmake)
conan_cmake_run(
CONANFILE conanfile.txt
BASIC_SETUP
BUILD missing
NO_OUTPUT_DIRS
)
find_package(Python3 COMPONENTS Interpreter Development)
include_directories(${PYTHON_INCLUDE_DIRS})
include_directories(${Python3_INCLUDE_DIRS})
find_package(pybind11 REQUIRED)
pybind11_add_module(${PROJECT_NAME} main.cpp )
target_include_directories(
${PROJECT_NAME}
PRIVATE
${NUMPY_ROOT}/include
${PROJECT_SOURCE_DIR}/vendor/General_NetSDK_Eng_Linux64_IS_V3.051
${PROJECT_SOURCE_DIR}/vendor/ffmpeg4.2.1
)
target_link_libraries(
${PROJECT_NAME}
PRIVATE
${CONAN_LIBS}
)
Semua persiapan sudah selesai, yuk kumpulkan:
cmake . -DCMAKE_BUILD_TYPE=Release
cmake --build . --parallel 2
Kami menarik kesimpulan:
- Kami menerima pustaka biner rakitan, yang selanjutnya dapat dimuat ke interpreter Python dengan caranya.
- Jauh lebih mudah untuk mengekspor kode ke Python dibandingkan dengan metode di atas, dan kode pembungkus menjadi lebih ringkas dan ditulis dalam bahasa yang sama.
Salah satu fitur cpython / pybind11 adalah memuat, mendapatkan, atau menjalankan fungsi dari runtime python saat berada di runtime C ++ dan sebaliknya.
Mari kita lihat contoh sederhana:
#include <pybind11/embed.h> //
namespace py = pybind11;
int main() {
py::scoped_interpreter guard{}; // python vm
py::print("Hello, World!"); // Hello, World!
}
Dengan menggabungkan kemampuan untuk menyematkan interpreter python dalam aplikasi C ++ dan mesin modul Python, kami menemukan pendekatan yang menarik dimana kode mesin ipyparalles tidak merasakan substitusi komponen. Untuk aplikasi, kami memilih arsitektur di mana siklus kehidupan dan peristiwa dimulai dalam kode C ++, dan baru kemudian interpreter Python dimulai dalam proses yang sama.
Untuk memahami, mari kita lihat bagaimana pendekatan kami bekerja:
#include <pybind11/embed.h>
#include "pyrectangle.hpp" // ++ rectangle
using namespace py::literals;
// rectangle
constexpr static char init_script[] = R"__(
import sys
sys.modules['rect'] = rect
)__";
// rectangle
constexpr static char load_script[] = R"__(
import sys, os
from importlib import import_module
sys.path.insert(0, os.path.dirname(path))
module_name, _ = os.path.splitext(path)
import_module(os.path.basename(module_name))
)__";
int main() {
py::scoped_interpreter guard; //
py::module pyrectangle("rect");
add_pyrectangle(pyrectangle); //
py::exec(init_script, py::globals(), py::dict("rect"_a = pyrectangle)); // Python.
py::exec(load_script, py::globals(), py::dict("path"_a = "main.py")); // main.py
return 0;
}
Dalam contoh di atas, modul pyrectangle diteruskan ke interpreter Python dan tersedia untuk diimpor sebagai persegi. Mari kita tunjukkan dengan contoh bahwa tidak ada yang berubah untuk kode "khusus":
from pprint import pprint
from rect import PyRectangle
r = PyRectangle(0, 3, 5, 8)
pprint(r)
assert r.getArea() == 25
width, height = r.getSize()
assert width == 5 and height == 5
Pendekatan ini dicirikan oleh fleksibilitas tinggi dan banyak titik penyesuaian, serta kemampuan untuk mengelola memori Python secara legal. Tetapi ada masalah - biaya kesalahan jauh lebih tinggi daripada opsi lain, dan Anda perlu mewaspadai risiko ini.
Jadi, ctypes dan CFFI tidak cocok untuk kita karena kebutuhan untuk mengekspor antarmuka pustaka gaya-C, dan juga karena kebutuhan untuk menulis pembungkus di sisi Python dan, pada akhirnya, menggunakan API CPython jika penyematan diperlukan. Cython bebas dari cacat ekspornya, tetapi tetap mempertahankan semua kekurangan lainnya. Pybind11 hanya mendukung penyematan dan penulisan pembungkus di sisi C ++. Ia juga memiliki kemampuan ekstensif untuk memanipulasi struktur data dan memanggil fungsi dan metode Python. Hasilnya, kami menetapkan pybind11 sebagai pembungkus C ++ level tinggi untuk CPython API.
Dengan menggabungkan penggunaan embed python di dalam aplikasi C ++ dengan mekanisme modul untuk penerusan data yang cepat dan menggunakan kembali basis kode mesin ipyparallel, kami mendapatkan rocketjoe_engine. Ini identik dalam mekanika dengan aslinya dan bekerja lebih cepat dengan mengurangi kasta untuk interaksi jaringan, pemrosesan json, dan tindakan perantara lainnya. Sekarang ini memungkinkan teman saya untuk menyimpan banyak produksi, yang mana saya menerima bintang pertama dalam proyek GitHub .
Conan, Russian Python Week C++, Python Conan .
Russian Python Week 4 β 14 17 . , Python: Python- . , Python.
.