D言語(mir)でNumPyを拡張する

## はじめに

この記事ではD言語のmirというライブラリを使って，簡単にnumpy配列を処理する方法を提案します．D言語のmixinやUDAのおかげで3行追加するだけでD言語で書いた関数がpythonで使えるようになります。

元ネタ:　[RustでNumPyを拡張する](https://qiita.com/termoshtt/items/efc9ecd4ca2e777b149e)

## 何故D？

Dは現代的なシステム言語で数値計算に向いている（と思う）
 
- C++/Javaによく似た文法
- C++的なメモリモデル・move semantics (no GCも可能)
- SFINAEを使わない小奇麗なTemplate
- 強力な型推論
- Native Thread
- LLVMによる最適化 (LDC)
- Dで書きたいから

D言語に興味があるPythonユーザの方は次の記事をおすすめです．

- https://postd.cc/d-for-the-win
- https://postd.cc/d-is-for-data-science

なお，D言語にはすでにNumpyのような多次元配列ライブラリ[Mir](https://github.com/libmir/)が存在し，Pythonの効率が良い配列の共有方法[Buffer Protocol](https://docs.python.org/ja/3/c-api/buffer.html)の実装もありました．


## やったこと

ただ実際には次のような追加のライブラリが必要だったので作りました

- Python側のNumpy配列オブジェクト等からBuffer Protocolへの変換[ライブラリ](https://pypi.org/project/pybuffer/)
- 良い感じにmir.ndslice.Sliceを引数にとる関数に`@pybuffer`とアノテーションするとBuffer Protocol用のラッパー関数を生成する[ライブラリ](https://code.dlang.org/packages/mir-pybuffer)

GitHub: https://github.com/ShigekiKarita/mir-pybuffer

## インストール

Python側にはpipでインストールします

``` console
$ pip install pybuffer
```

D言語のプロジェクトファイルdub.jsonにはパッケージの追加と動的ライブラリ生成のオプションが必要です．例えば次のように書きます．

``` dub.json
{
	"name": "your-dub-lib",
	"authors": ["you"],
	"dependencies": {
		"mir-pybuffer": "~>0.1.7"
	},
	"targetType": "dynamicLibrary",
	"description": "A minimal D application.",
	"copyright": "Copyright © 2018, you",
	"license": "BSL-1.0"
}
```

## D言語側の使い方

まずD言語側の関数の書き方です．この例ではdoubleの行列matの一列目にベクトルvecを足して，vecをa倍する関数です．これに追加で3行加えるとPythonから使えるようになります．

1. `import pybuffer : pybuffer, MixinPyBufferWrappers;
`でmir-pybufferパッケージを読み込む
2. `@pybuffer`としてBuffer Protocolに対応した関数を生成するようにマークする
3. `mixin MixinPyBufferWrappers;`で自動的にSliceを[Py_buffer構造体](https://docs.python.org/ja/3/c-api/buffer.html#c.Py_buffer)に変換するラッパー関数を生成する

``` d
import mir.ndslice : Slice, Contiguous;
import pybuffer : pybuffer, MixinPyBufferWrappers; // 1

@pybuffer // 2
void func1(Slice!(Contiguous, [2LU], double*) mat, Slice!(Contiguous, [1LU], double*) vec, double a) {
  mat[0][] += vec;
  vec[] *= a;
}

mixin MixinPyBufferWrappers;
```

まだ開発中のライブラリなので注意点が2つあります．

1. `@pybuffer`はPy_buffer -> Slice変換時のエラーコードを返す関数を作るので，元関数の戻り値はvoidでないとコンパイルエラーになります．
2. `import pybuffer : pybuffer, MixinPyBufferWrappers;`でなく`import pybuffer;`と書くとコードが生成されません．原因は調査中です．
3. ~~`@pybuffer`された関数の中で，GCを使うと固まります．原因は調査中です．ただしmirはほぼ全てのAPIがGCなしで動くのであまり問題にはならないと思います．~~ 
  使えるようになりました(v0.1.7) thanks [@lempiji](https://twitter.com/lempiji/status/981379623702409219)

## Python側の使い方

こちらはとくに注意点はなく，普通にctypesを使ってdubによって生成されたD言語の動的ライブラリを読み込むだけです．

``` python
import ctypes
import numpy
import pybuffer

x = numpy.array([[0, 1, 2], [3, 4, 5]]).astype(numpy.float64)
y = numpy.array([0, 1, 2]).astype(numpy.float64)

lib = pybuffer.CDLL("./libyour-dub-lib.so")
err = lib.func1(x, y, ctypes.c_double(3.0))

assert err == 0
assert x == numpy.array([[0, 2, 4], [3, 4, 5]])
assert y == numpy.array([0, 3, 6])
```

[ctypes.CDLL](https://docs.python.jp/3/library/ctypes.html#ctypes.CDLL)と違ってpybuffer.CDLLクラスは自動変換された関数を優先して呼び出し，numpy配列などは自動的にPy_buffer構造体に変換されます．

開発用ですが，実際に生成されたラッパー関数は `lib.print_generated()` で表示することができます．挙動がおかしいときは確認して[GitHub](https://github.com/ShigekiKarita/mir-pybuffer/issues)までご連絡ください．

## おわりに

いかがでしょうか、D言語にはGCや強力なメタプログラミング機能があるので，他の言語よりも遥かに簡単にnumpyの多次元配列が処理できると思います。今ならライブラリ本体も100行くらいで、とても簡単なのでPR等もお待ちしています。

## Appendix

全体のサンプルコードはこちらを見てください

https://github.com/ShigekiKarita/mir-pybuffer/tree/master/test

D言語ではなく，C言語でPy_buffer構造体を読み書きする例もあります．