LoginSignup
2
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-09-04

はじめに

最近仕事で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で渡します。


  1. この時点で思いっきり仕事の延長や笑 

  2. これはpyppmdのテストデータと実質同じものです(なぜかpyppmdのファイルは改行コード変換が発生していますが)。 

  3. ライセンスは https://en.wikipedia.org/wiki/Historical_Permission_Notice_and_Disclaimer に該当する模様 

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0