Python Pickle フォーマットのメモ

# 背景

機械学習やレイトレーシングなどで, Python と C/C++ でバイナリデータのやりとりをしたい.
Python の標準機能だけで完結したい. text なら JSON や numpy text 形式(csv)などあるが, バイナリは C++ 側でお手軽に使えるのは無い. 

Pickle シリアリゼーションを検討する.

https://docs.python.org/ja/3/library/pickle.html

endianness も考慮されているようです.

## 情報

Pickle のシリアリゼーションフォーマット自体をお手軽に解説したサイトは英語でもありませんでした. :cry:
(わかってしまえば, そんなに複雑なフォーマットではないので, 解説するほどでもないのかもしれませんが...)

しかし, ありがたいことに PyTorch JIT が, TorchScript(Python ライクなスクリプト言語)を実装するうえで, 自前の C++ Pickle ローダとシリアリゼーション対応をしていて, コードが参考になります.

https://github.com/pytorch/pytorch/blob/master/torch/csrc/jit/docs/serialization.md

https://github.com/pytorch/pytorch/blob/master/torch/csrc/jit/serialization/pickler.h

また, Python の Pickletools でデータの解析ができます.

https://docs.python.org/ja/3.6/library/pickletools.html

## フォーマット

### Protocol バージョン

Pickle にはいくつか Protocol バージョンがあります. Python3 では 3 がデフォルトですが, Python3 で proto 3 でシリアライズすると, Python2 で読むことができません. 

数値データメインで, あんまりへんてこでないデータを扱わないのであれば, proto 2 が推奨でしょうか.
(TorchScript では proto 2 しかサポートしていない)

ヘッダは, `0x80`(PROTO, 1 byte), バージョン番号(1 byte)の 2 bytes になります.

ためしに 1 をシリアライズしてみます.

```
import pickle
import io

a = 1 

f = io.BytesIO()
b = pickle.dump(a, f)

w = open("bora.p", "wb")
w.write(f.getbuffer())
```

```
$ od -tx1c bora.p
0000000  80  03  4b  01  2e
        200 003   K 001   .
0000005
```

'K' は `BININT1`
`.`(2e) は `STOP` です. データの終わりです.

pytorch jit の unpicker.cpp を見ると,

```
    case PickleOpCode::BININT1: {
      uint8_t value = read<uint8_t>();
      stack_.emplace_back(int64_t(value));
    } break;
```

とありますので, `BININT1` は 1 byte でシリアライズできる int 型の値であることがわかります.

配列データを試してみます.

```
import pickle
import io

a = [1, 2] 

f = io.BytesIO()
b = pickle.dump(a, f, protocol=2)

w = open("bora.p", "wb")
w.write(f.getbuffer())
```

こんどは pickletools でダンプしてみます.

```
$ python -m pickletools bora.p 
    0: \x80 PROTO      2
    2: ]    EMPTY_LIST
    3: q    BINPUT     0
    5: (    MARK
    6: K        BININT1    1
    8: K        BININT1    2
   10: e        APPENDS    (MARK at 5)
   11: .    STOP
highest protocol among opcodes = 2
```

基本的には, prefix + 実際のデータの組み合わせというかたちなので, あとは pytorch jit の pickler.cpp, unpickler.cpp や, pickletools.py を参考にいろいろ試していって解析すればよさそうです!

## numpy array

numpy array(ndarray) をシリアライズしてみます.

```py
a = numpy.array([1.0, 2.2, 3.3, 4, 5, 6, 7, 8, 9, 10], dtype=numpy.float32)

f = io.BytesIO()
b = pickle.dump(a, f, protocol=2)

w = open("bora.p", "wb")
w.write(f.getbuffer())
```

```
    0: \x80 PROTO      2
    2: c    GLOBAL     'numpy.core.multiarray _reconstruct'
   38: q    BINPUT     0
   40: c    GLOBAL     'numpy ndarray'
   55: q    BINPUT     1
   57: K    BININT1    0
   59: \x85 TUPLE1
   60: q    BINPUT     2
   62: c    GLOBAL     '_codecs encode'
   78: q    BINPUT     3
   80: X    BINUNICODE 'b'
   86: q    BINPUT     4
   88: X    BINUNICODE 'latin1'
   99: q    BINPUT     5
  101: \x86 TUPLE2
  102: q    BINPUT     6
  104: R    REDUCE
  105: q    BINPUT     7
  107: \x87 TUPLE3
  108: q    BINPUT     8
  110: R    REDUCE
  111: q    BINPUT     9
  113: (    MARK
  114: K        BININT1    1
  116: K        BININT1    10
  118: \x85     TUPLE1
  119: q        BINPUT     10
  121: c        GLOBAL     'numpy dtype'
  134: q        BINPUT     11
  136: X        BINUNICODE 'f4'
  143: q        BINPUT     12
  145: K        BININT1    0
  147: K        BININT1    1
  149: \x87     TUPLE3
  150: q        BINPUT     13
  152: R        REDUCE
  153: q        BINPUT     14
  155: (        MARK
  156: K            BININT1    3
  158: X            BINUNICODE '<'
  164: q            BINPUT     15
  166: N            NONE
  167: N            NONE
  168: N            NONE
  169: J            BININT     -1
  174: J            BININT     -1
  179: K            BININT1    0
  181: t            TUPLE      (MARK at 155)
  182: q        BINPUT     16
  184: b        BUILD
  185: \x89     NEWFALSE
  186: h        BINGET     3
  188: X        BINUNICODE '\x00\x00\x80?ÍÌ\x0c@33S@\x00\x00\x80@\x00\x00\xa0@\x00\x00À@\x00\x00à@\x00\x00\x00A\x00\x00\x10A\x00\x00 A'
  240: q        BINPUT     17
  242: h        BINGET     5
  244: \x86     TUPLE2
  245: q        BINPUT     18
  247: R        REDUCE
  248: q        BINPUT     19
  250: t        TUPLE      (MARK at 113)
  251: q    BINPUT     20
  253: b    BUILD
  254: .    STOP
highest protocol among opcodes = 2
```

BINUNICODE あたりにバイト列で配列データが格納されているのがわかります.
numpy のソースコードあたりを解析したら自前 C++ ローダで pickle 版の numpy 配列や pytorch tensor(numpy と似た構造を持っていると想像できる)を読み込めそうですね!
(numpy ネイティブ? の NPY/NPZ はフォーマットがいくらか簡潔で, たとえば cnpy で読み書きできる https://github.com/rogersce/cnpy ) 

## TODO

* [ ] ついでなので, 自前で Python っぽいインタプリタも実装してみたい.
* [ ] 優秀な Python 若人さまが, **Pickle フォーマットを極めることにより, 人類史上最速で優秀な C++ で Picke データローダ若人さまへと昇華なされるスキーム** を確立する旅に出たい.