園児ニア日記

MessagePackの新しいRust実装、messagepack-rsをリリースしました

もともとは独自のシリアライズフォーマットを作る予定だった

正確に言うと予定だったではなく既に仕様も決まりRustでの実装も完了していた。
なんでMessagePackなどの既存のものではなく新しいフォーマットを作ったかと言うと、いくつか理由があってだいたいこんな感じ↓

新しいフォーマットを作った理由

  1. MessagePackの仕様上Map型のkeyにはどんな型の値でも入れられるようになっているのだけど、Rustの場合HashMapやBTreeMapのkeyにf32やf64のような浮動小数点数型を入れられないという問題がある(MessagePackのRustの実装の一つであるRMPはMap型をVec<(Value, Value)>で実装するという力技をやっている)
  2. MessagePackはストリーム処理ができないと思っていた(完全に見落としていただけでちゃんとストリーム処理できる)
  3. Extension型を作る場合必ず1バイト分オーバーヘッドができてしまう(その型がExtension型であることを示すための1バイト)
  4. BigInt型、BigDecimal型、Date型などが定義されていない

これらの理由から独自のフォーマットとその実装を作ったのだけど、完成したあたりでふと↓のような解決策?(特にMapのやつ)に気がついてしまい、新しくフォーマット作ったらドキュメント作ったりとか、言語ごとにライブラリ作らないといけないし、既存のMessagePackを使ったほうがよいのでは?と思い、独自フォーマットを泣く泣く捨てる形になった。

気がついてしまった解決策(?)

  1. Mapのキーに文字列しか入れられないようにする
  2. Extension型の1バイト分のオーバーヘッドはMessagePackの他のサイズ削減機能(fixXXX系のヘッダー部分にデータを入れられる機能)で多くの場合相殺できる
  3. BigInt型、BigDecimal型、Date型はExtension型として自分で定義すれば使える(個人的にこれらの型は標準で対応してもらえると嬉しい…)

MessagePackの新しいRust実装を作ることに

既存のRust実装であるRMPは軽く見た感じ3つの問題があるように感じた。

  1. 上の方で書いたようにMap型をVec<(Value, Value)>で実装している(MessagePackの仕様をちゃんと守っているとも言える)
  2. Timestamp型に対応しておらず、2019年8月現在、型情報に負の値を入れられない(Timestamp型の型番号は-1)ため自分でTimestamp型を実装することもできず、それに対するissueも1年近く放置されている
  3. ストリームに対応していない(多分…使い方わかってないだけで実は対応していたらすみません…)

これらの問題がない実装が個人的に必要だったので新しくmessagepack-rsを作成した。

messagepack-rsの特徴

  1. Map型をBTreeMap<String, Value>で実装(HashMapではなくBTreeMapな理由は文字列として出力したときにHashMapだと順番がぐちゃぐちゃになってしまい見にくいため)
  2. Timestamp型に対応
  3. ストリームに対応

messagepack-rsの使い方

基本的な使い方

ストリーム

Extension(定義が簡単で使うときにちょっと面倒くさい方法)

Extension(定義が少し面倒だけど使うときに楽な方法)

だいたいこんな感じで使える。
まだSerdeに対応してないのでExtensionの定義が面倒だけどそのうち対応予定。
僕と同じような理由でRMPが利用できない場合、試しに使ってみてもらえたら嬉しいです🤗

RubyのFiddleやFFIでOpaque Pointerを扱う

とりあえずRustで簡単なShared Libraryを用意する

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

pub type StringArray = Vec<CString>;

#[no_mangle]
pub extern fn string_array_new() -> *mut StringArray {
    Box::into_raw(Box::new(StringArray::new()))
}

#[no_mangle]
pub extern fn string_array_destroy(string_array: *mut StringArray) {
    unsafe {
        Box::from_raw(string_array);
    }
}

#[no_mangle]
pub extern fn string_array_add(string_array: *mut StringArray, value: *const c_char) {
    unsafe {
        (*string_array).push(CStr::from_ptr(value).to_owned());
    }
}

#[no_mangle]
pub extern fn string_array_for_each(string_array: *const StringArray, callback: extern fn(*const c_char)) {
    unsafe {
        (*string_array).iter().for_each(|value| callback(value.as_ptr()));
    }
}

↑のコードを cargo build --release すると target/releaselibstringarray.so ができるので適当なところにコピーする

FiddleでOpaque Pointerを扱う

とりあえずFiddleの公式ドキュメントに書いてあるstructを使ってみる

require 'fiddle/import'

module StringArrayFiddle
  extend Fiddle::Importer
  dlload './libstringarray.so'

  StringArray = struct([])
end
$ bundle exec ruby main.rb
Traceback (most recent call last):
	6: from main.rb:3:in `<main>'
	5: from main.rb:7:in `<module:StringArrayFiddle>'
	4: from /usr/lib/ruby/2.6.0/fiddle/import.rb:224:in `struct'
	3: from /usr/lib/ruby/2.6.0/fiddle/struct.rb:64:in `create'
	2: from /usr/lib/ruby/2.6.0/fiddle/struct.rb:112:in `size'
	1: from /usr/lib/ruby/2.6.0/fiddle/pack.rb:55:in `align'
/usr/lib/ruby/2.6.0/fiddle/pack.rb:55:in `%': nil can't be coerced into Integer (TypeError)

これは動きません!
Fiddleのstructは最低でも一つは要素がないと使えないのです。
困りましたね。ドキュメントを見てもstruct以外に構造体を宣言できそうなものがありません。

答え 宣言などせずにいきなり使う

宣言無しでいきなり使うと動きます。

require 'fiddle/import'

module StringArrayFiddle
  extend Fiddle::Importer
  dlload './libstringarray.so'

  extern 'StringArray* string_array_new(void)'
  extern 'void string_array_destroy(StringArray *string_array)'
  extern 'void string_array_add(StringArray *string_array, const char* value)'
  extern 'void string_array_for_each(const StringArray *string_array, void (*callback)(const char*))'
end

# 以下動作確認用コード
string_array = StringArrayFiddle.string_array_new
StringArrayFiddle.string_array_add(string_array, 'aaaa')
StringArrayFiddle.string_array_add(string_array, 'bbbb')
callback = Fiddle::Closure::BlockCaller.new(Fiddle::TYPE_VOID, [Fiddle::TYPE_VOIDP]) { |str| puts str }
StringArrayFiddle.string_array_for_each(string_array, callback)
StringArrayFiddle.string_array_destroy(string_array)
$ bundle exec ruby main.rb
aaaa
bbbb

FFIでOpaque Pointerを扱う

次はFFIでOpaque Pointerを扱うやり方
FFIの公式ドキュメントにあるように FFI::Struct を継承してあげればできそうですね

require 'bundler/setup'
require 'ffi'

module StringArrayFFI
  extend FFI::Library
  ffi_lib './libstringarray.so'

  class StringArray < FFI::Struct; end
  attach_function :string_array_new, [], StringArray.by_ref
end

# 以下動作確認用コード
string_array = StringArrayFFI.string_array_new
$ bundle exec ruby main.rb
Traceback (most recent call last):
	3: from main.rb:33:in `<main>'
	2: from main.rb:33:in `string_array_new'
	1: from main.rb:33:in `from_native'
main.rb:33:in `initialize': no Struct layout configured for StringArrayFFI::StringArray (RuntimeError)

これも動きません!
Fiddle同様FFIも最低でも一つは要素がないと使えないようです。

答え :pointerを使う

require 'bundler/setup'
require 'ffi'

module StringArrayFFI
  extend FFI::Library
  ffi_lib './libstringarray.so'

  attach_function :string_array_new, [], :pointer
  attach_function :string_array_destroy, [:pointer], :void
  attach_function :string_array_add, [:pointer, :string], :void
  callback :string_array_for_each_callback, [:string], :void
  attach_function :string_array_for_each, [:pointer, :string_array_for_each_callback], :void
end

# 以下動作確認用コード
string_array = StringArrayFFI.string_array_new
StringArrayFFI.string_array_add(string_array, 'aaaa')
StringArrayFFI.string_array_add(string_array, 'bbbb')
StringArrayFFI.string_array_for_each(string_array, -> str { puts str })
StringArrayFFI.string_array_destroy(string_array)
$ bundle exec ruby main.rb
aaaa
bbbb

LLVM-Cで最小(?)の実行形式ファイルを作る

LLVM (7.0.1) を利用して3を返すだけのプログラムをLLVM-Cで作る

将来的にセルフホスティング可能な自作言語を作りたいので、移植しやすいようにLLVM-Cを使って、とりあえず簡単なプログラムを作成してみる。
内容はmain関数で3を返すだけのもの。

minimum_llvm_c.c

#include <llvm-c/Core.h>
#include <llvm-c/ExecutionEngine.h>
#include <stdlib.h>

int main (int argc, char const *argv[]) {
  LLVMInitializeNativeTarget();
  LLVMModuleRef mod = LLVMModuleCreateWithName("minimum_llvm_c");
  LLVMValueRef function = LLVMAddFunction(mod, "main", LLVMFunctionType(LLVMInt32Type(), NULL, 0, 0));
  LLVMSetFunctionCallConv(function, LLVMCCallConv);
  LLVMBasicBlockRef block = LLVMAppendBasicBlock(function, "");
  LLVMBuilderRef builder = LLVMCreateBuilder();
  LLVMPositionBuilderAtEnd(builder, block);
  LLVMBuildRet(builder, LLVMConstInt(LLVMInt32Type(), 3, 0));

  char *errorMessage = NULL;
  LLVMPrintModuleToFile(mod, "minimum_llvm_c.ll", &errorMessage);
  LLVMDisposeBuilder(builder);
  return 0;
}

↑のコードに対して↓のコマンドを実行するとLLVM IR (minimum_llvm_c.ll) が作成される

clang `llvm-config --cflags` -c minimum_llvm_c.c
clang minimum_llvm_c.o `llvm-config --libs --cflags --ldflags core executionengine native` -o minimum_llvm_c
./minimum_llvm_c

minimum_llvm_c.ll

; ModuleID = 'minimum_llvm_c'
source_filename = "minimum_llvm_c"

define i32 @main() {
  ret i32 3
}

最後に↓のコマンドでminimum_llvm_c.llから実行ファイルを作成後、実行して終了ステータスで3が返っているか確認する。

llc minimum_llvm_c.ll
clang minimum_llvm_c.s
./a.out
echo $?
3