はじめに
最近仕事でpybind11をよくさわっておりまして。(binding部分)フルスクラッチでpybind11を使ったPythonライブラリを作ってみたいと思いました。ついでにpybind11と似た方法でRubyライブラリを作れるRiceにも触れておきたいと思いました。
TL;DR
作るもの
libslzのバインディングとします1。C++のクラスのコンストラクタでslz_initを呼び出し、compressメソッドでslz_encode、flushメソッドでslz_finishを呼び出すとします。
テストコードは、libslzの返した値をzlibで展開できれば良いものとします。圧縮前のデータには http://eforexcel.com/wp/downloads-18-sample-csv-files-data-sets-for-testing-sales/ の「10000 Sales Records」の展開したものを使います2。
Python
C++
- 骨子としては、
std::string#reserve
でバッファを確保してその中に書き込んで頂く。確保する量はlen(obj)+len(obj)//16
(と現在のバッファサイズとの大きい方)とする。 - objからchar*を取得するにはPYBIND11_BYTES_AS_STRING_AND_SIZEを使う。
-
py::bytes(ptr, size)
とすることで任意のバッファからbytesを作成できる。 - メソッド宣言は
py::class_#def
。 - 定数宣言はm.attr。
#include <pybind11/pybind11.h>
extern "C" {
#include "libslz/src/slz.h"
}
namespace py = pybind11;
using namespace pybind11::literals;
class slz_compressobj{
slz_stream strm;
std::string out;
int outsize;
public:
slz_compressobj(int level=1, int format=SLZ_FMT_DEFLATE): outsize(0){
slz_init(&strm, level, format);
}
py::bytes compress(const py::bytes &obj){
int tempoutsize = py::len(obj)+py::len(obj)/16;
if(outsize < tempoutsize){
outsize = tempoutsize;
out.reserve(outsize);
}
//out.resize(tempoutsize);
size_t written = 0;
{
char *buffer = nullptr;
ssize_t length = 0;
PYBIND11_BYTES_AS_STRING_AND_SIZE(obj.ptr(), &buffer, &length);
written = slz_encode(&strm, &out[0], buffer, length, 1);
}
//out.resize(written);
return py::bytes(out.data(), written);
}
py::bytes flush(){
int tempoutsize = 12;
if(outsize < tempoutsize){
outsize = tempoutsize;
out.reserve(outsize);
}
//out.resize(tempoutsize);
size_t written = slz_finish(&strm, &out[0]);
//out.resize(written);
return py::bytes(out.data(), written);
}
};
PYBIND11_MODULE(slz, m){
py::class_<slz_compressobj, std::shared_ptr<slz_compressobj> >(m, "compressobj")
.def(py::init<int, int>(), "level"_a=1, "format"_a=int(SLZ_FMT_DEFLATE))
.def("compress", &slz_compressobj::compress,
"obj"_a
)
.def("flush", &slz_compressobj::flush)
;
m.attr("SLZ_FMT_GZIP") = int(SLZ_FMT_GZIP);
m.attr("SLZ_FMT_ZLIB") = int(SLZ_FMT_ZLIB);
m.attr("SLZ_FMT_DEFLATE") = int(SLZ_FMT_DEFLATE);
}
setup.py
C拡張はsetupのext_modules引数で指定します。
pybind11のsetup helperはpyproject.tomlでインストールすることにしました。
C/C++混在
Pythonの拡張モジュールでCとC++を混在させるのはかなり面倒である。同じ実行オプションが渡るためである(-std=c++11とかがCに渡るとエラーになる)。
独自build_ext中で勝手にビルドすることも可能だと思うが、Pythonのビルドシステムを活用したいならば、オプション解釈部を書き換えねばならない。
今回はPyMOLの実装をお借りすることとした3。パッケージングとしては、pybind11 setup.pyチュートリアルと同様にMANIFEST.inに記載すれば良い。
Ruby
extconf
Riceはバージョン間の非互換が割と激しい感じがあるが、古いRubyを簡単に切ったりするので、互換性をもたせたいならばC++に分岐を置く必要がある。その分岐を持たせるには、Gem.loaded_specs["rice"].version.to_s
を加工してCXXFLAGSに追加すれば良い。
require 'bundler/setup'
#require 'rice'
require 'mkmf-rice'
rice_version_a = Gem.loaded_specs["rice"].version.to_s.split('.').map(&:to_i)
RICE_VERSION = rice_version_a[0]*10000 + rice_version_a[1]*100 + rice_version_a[2]
$CXXFLAGS += " -DRICE_VERSION=#{RICE_VERSION}"
$srcs = ['ext/rbslz.cpp', 'ext/libslz/src/slz.c']
$VPATH << 'ext' << 'ext/libslz/src'
create_makefile('slz')
C++
- objからchar*を取得するには
obj.c_str()
を使う。 - 任意のバッファからStringを作成する関数はないので、String.hppを見ながら見様見真似で実装。
- 定数宣言はconst_set。
- Pythonはモジュール名をインポートするけどRubyはそうじゃないので、モジュールを宣言してその下にクラスを置くことが多いと思う。define_moduleの返し値をdefine_class_underの引数として渡せば良い。
- メソッド宣言は
define_class_under#define_method
。
- インクルードするヘッダがRice 3以下と4以上で異なる。
- protect/to_ruby/コンストラクタの宣言がRice 2以下と3以上で異なる。
- 条件分岐は上記CXXFLAGSによりなされる。
#if RICE_VERSION >= 40000
#include <rice/rice.hpp>
#else
#include <rice/Class.hpp>
#include <rice/Constructor.hpp>
#include <rice/Module.hpp>
#include <rice/String.hpp>
#endif
extern "C" {
#include "libslz/src/slz.h"
}
#if RICE_VERSION >= 30000
#define RICE_PROTECT Rice::detail::protect
#define RICE_TORUBY Rice::detail::to_ruby
#define RICE_CTOR_PARENSL
#define RICE_CTOR_PARENSR
#else
#define RICE_PROTECT Rice::protect
#define RICE_TORUBY to_ruby
#define RICE_CTOR_PARENSL (
#define RICE_CTOR_PARENSR )
#endif
static inline Rice::String MakeStringFromBuffer(const char *ptr, long len){
return Rice::Builtin_Object<T_STRING>(RICE_PROTECT(rb_str_new, ptr, len));
}
class slz_compressobj{
slz_stream strm;
std::string out;
int outsize;
public:
slz_compressobj(int level=1, int format=SLZ_FMT_DEFLATE): outsize(0){
slz_init(&strm, level, format);
}
Rice::String compress(Rice::String obj){
int tempoutsize = obj.length()+obj.length()/16;
if(outsize < tempoutsize){
outsize = tempoutsize;
out.reserve(outsize);
}
//out.resize(tempoutsize);
size_t written = 0;
written = slz_encode(&strm, &out[0], obj.c_str(), obj.length(), 1);
//out.resize(written);
return MakeStringFromBuffer(out.data(), written);
}
Rice::String flush(){
int tempoutsize = 12;
if(outsize < tempoutsize){
outsize = tempoutsize;
out.reserve(outsize);
}
//out.resize(tempoutsize);
size_t written = 0;
written = slz_finish(&strm, &out[0]);
//out.resize(written);
return MakeStringFromBuffer(out.data(), written);
}
};
extern "C"
void Init_slz()
{
auto module = Rice::define_module("Slz")
.const_set("SLZ_FMT_GZIP", RICE_TORUBY((int)SLZ_FMT_GZIP))
.const_set("SLZ_FMT_ZLIB", RICE_TORUBY((int)SLZ_FMT_ZLIB))
.const_set("SLZ_FMT_DEFLATE", RICE_TORUBY((int)SLZ_FMT_DEFLATE))
;
Rice::define_class_under<slz_compressobj>(module,"Deflate")
.define_constructor(Rice::Constructor<slz_compressobj, int, int>(), RICE_CTOR_PARENSL Rice::Arg("level")=1, Rice::Arg("format")=(int)SLZ_FMT_DEFLATE RICE_CTOR_PARENSR)
.define_method("deflate", &slz_compressobj::compress)
.define_method("flush", &slz_compressobj::flush)
;
}
gemspec
spec.extensions
にextconf.rbの一覧をArrayで渡します。
-
この時点で思いっきり仕事の延長や笑 ↩
-
これはpyppmdのテストデータと実質同じものです(なぜかpyppmdのファイルは改行コード変換が発生していますが)。 ↩
-
ライセンスは https://en.wikipedia.org/wiki/Historical_Permission_Notice_and_Disclaimer に該当する模様 ↩