pybind11とRiceを比較しながら触ってみる


## はじめに

最近仕事でpybind11をよくさわっておりまして。(binding部分)フルスクラッチでpybind11を使ったPythonライブラリを作ってみたいと思いました。ついでにpybind11と似た方法でRubyライブラリを作れるRiceにも触れておきたいと思いました。

## TL;DR

- https://github.com/cielavenir/python-slz
- https://github.com/cielavenir/ruby-slz

## 作るもの

[libslz](http://www.libslz.org/)のバインディングとします[^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。

```cpp
#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の実装](https://github.com/schrodinger/pymol-open-source/blob/master/monkeypatch_distutils.py)をお借りすることとした[^3]。パッケージングとしては、[pybind11 setup.pyチュートリアル](https://pybind11.readthedocs.io/en/stable/compiling.html)と同様にMANIFEST.inに記載すれば良い。

## Ruby

### extconf

Riceはバージョン間の非互換が割と激しい感じがあるが、古いRubyを簡単に切ったりするので、互換性をもたせたいならばC++に分岐を置く必要がある。その分岐を持たせるには、`Gem.loaded_specs["rice"].version.to_s`を加工してCXXFLAGSに追加すれば良い。

```rb
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によりなされる。

```cpp
#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で渡します。

[^1]: この時点で思いっきり仕事の延長や笑
[^2]: これは[pyppmd](https://github.com/miurahr/pyppmd)のテストデータと実質同じものです(なぜかpyppmdのファイルは改行コード変換が発生していますが)。
[^3]: ライセンスは https://en.wikipedia.org/wiki/Historical_Permission_Notice_and_Disclaimer に該当する模様