Crystal binding for yajl JSON library

この記事は [Crystal Advent Calendar 2018](https://qiita.com/advent-calendar/2018/crystal)  の24日目の記事です。


今回は Crystal の [C Bindings][1] に関してあるライブラリをCrystalから呼び出せるようにしたいと思います。

### 対象のライブラリ
- https://github.com/lloyd/yajl

### C Bindings を生成するために利用するツール
- https://github.com/olbat/libgen

### 環境
- debian
- osx (未確認）

Libgen 用にコンフィグファイルを作成します。

- lib_yajl.yml

```yaml
---
name: LibYajl
ldflags: ""
cflags: "-I/usr/include/yajl"
packages: "yajl"
destdir: src/lib_yajl/
includes:
- include/yajl/*.yml
```

- include/yajl/yajl.yml

```yaml

---
description: Yajl
includes:
- /usr/include/yajl/yajl_common.h
- /usr/include/yajl/yajl_gen.h
- /usr/include/yajl/yajl_parse.h
- /usr/include/yajl/yajl_tree.h
- /usr/include/yajl/yajl_version.h
prefixes:
- yajl
```

今回 apt-get install yajl したため、　/usr/include　に yajl のヘッダファイルなどが入っています。
brew install yajl の場合は /usr/local/include になるかと思います。

- libgen を実行します

```
% libgen lib_yajl.yml
loading library from lib_yajl.yml
loading lib_yajl definition from include/yajl/lib_yajl.yml
/usr/include/yajl/yajl_common.h:20:10: fatal error: 'stddef.h' file not found
generate src/lib_yajl/lib_yajl.cr
```

- src/lib_yajl/lib_yajl.cr が生成されることが確認できます

```ruby

lib LibYajl
  YajlAllowComments         =  1
  YajlAllowMultipleValues   =  8
  YajlAllowPartialValues    = 16
  YajlAllowTrailingGarbage  =  4
  YajlDontValidateStrings   =  2
  YajlGenBeautify           =  1
  YajlGenEscapeSolidus      = 16
  YajlGenGenerationComplete =  4
  YajlGenInErrorState       =  3
  YajlGenIndentString       =  2
  YajlGenInvalidNumber      =  5
  YajlGenInvalidString      =  7
  YajlGenKeysMustBeStrings  =  1
  YajlGenNoBuf              =  6
  YajlGenPrintCallback      =  4
  YajlGenStatusOk           =  0
  YajlGenValidateUtf8       =  8
  YajlMaxDepthExceeded      =  2
  YajlStatusClientCanceled  =  1
  YajlStatusError           =  2
  YajlStatusOk              =  0
  YajlTAny                  =  8
  YajlTArray                =  4
  YajlTFalse                =  6
  YajlTNull                 =  7
  YajlTNumber               =  2
  YajlTObject               =  3
  YajlTString               =  1
  YajlTTrue                 =  5
  alias YajlFreeFunc = (Void*, Void* -> Void)
  alias YajlGenT = Void
  alias YajlHandleT = Void
  alias YajlMallocFunc = (Void*, LibC::Int -> Void*)
  alias YajlReallocFunc = (Void*, Void*, LibC::Int -> Void*)
  alias YajlVal = YajlValS*
  enum YajlGenOption
    YajlGenBeautify      =  1
    YajlGenIndentString  =  2
    YajlGenPrintCallback =  4
    YajlGenValidateUtf8  =  8
    YajlGenEscapeSolidus = 16
  end
  enum YajlGenStatus
    YajlGenStatusOk           = 0
    YajlGenKeysMustBeStrings  = 1
    YajlMaxDepthExceeded      = 2
    YajlGenInErrorState       = 3
    YajlGenGenerationComplete = 4
    YajlGenInvalidNumber      = 5
    YajlGenNoBuf              = 6
    YajlGenInvalidString      = 7
  end
  enum YajlOption
    YajlAllowComments        =  1
    YajlDontValidateStrings  =  2
    YajlAllowTrailingGarbage =  4
    YajlAllowMultipleValues  =  8
    YajlAllowPartialValues   = 16
  end
  enum YajlStatus
    YajlStatusOk             = 0
    YajlStatusClientCanceled = 1
    YajlStatusError          = 2
  end
  enum YajlType
    YajlTString = 1
    YajlTNumber = 2
    YajlTObject = 3
    YajlTArray  = 4
    YajlTTrue   = 5
    YajlTFalse  = 6
    YajlTNull   = 7
    YajlTAny    = 8
  end
  fun yajl_alloc(callbacks : YajlCallbacks*, afs : YajlAllocFuncs*, ctx : Void*) : YajlHandle
  fun yajl_complete_parse(hand : YajlHandle) : YajlStatus
  fun yajl_config(h : YajlHandle, opt : YajlOption, ...) : LibC::Int
  fun yajl_free(handle : YajlHandle)
  fun yajl_free_error(hand : YajlHandle, str : UInt8*)
  fun yajl_gen_alloc(alloc_funcs : YajlAllocFuncs*) : YajlGen
  fun yajl_gen_array_close(hand : YajlGen) : YajlGenStatus
  fun yajl_gen_array_open(hand : YajlGen) : YajlGenStatus
  fun yajl_gen_bool(hand : YajlGen, boolean : LibC::Int) : YajlGenStatus
  fun yajl_gen_clear(hand : YajlGen)
  fun yajl_gen_config(g : YajlGen, opt : YajlGenOption, ...) : LibC::Int
  fun yajl_gen_double(hand : YajlGen, number : LibC::Double) : YajlGenStatus
  fun yajl_gen_free(handle : YajlGen)
  fun yajl_gen_get_buf(hand : YajlGen, buf : UInt8**, len : LibC::Int*) : YajlGenStatus
  fun yajl_gen_integer(hand : YajlGen, number : LibC::LongLong) : YajlGenStatus
  fun yajl_gen_map_close(hand : YajlGen) : YajlGenStatus
  fun yajl_gen_map_open(hand : YajlGen) : YajlGenStatus
  fun yajl_gen_null(hand : YajlGen) : YajlGenStatus
  fun yajl_gen_number(hand : YajlGen, num : LibC::Char*, len : LibC::Int) : YajlGenStatus
  fun yajl_gen_reset(hand : YajlGen, sep : LibC::Char*)
  fun yajl_gen_string(hand : YajlGen, str : UInt8*, len : LibC::Int) : YajlGenStatus
  fun yajl_get_bytes_consumed : LibC::Int
  fun yajl_get_error(hand : YajlHandle, verbose : LibC::Int, json_text : UInt8*, json_text_length : LibC::Int) : UInt8*
  fun yajl_parse(hand : YajlHandle, json_text : UInt8*, json_text_length : LibC::Int) : YajlStatus
  fun yajl_status_to_string(code : YajlStatus) : LibC::Char*
  fun yajl_tree_free(v : YajlVal)
  fun yajl_tree_get(parent : YajlVal, path : LibC::Char**, type : YajlType) : YajlVal
  fun yajl_tree_parse(input : LibC::Char*, error_buffer : LibC::Char*, error_buffer_size : LibC::Int) : YajlVal
  fun yajl_version : LibC::Int

  struct YajlAllocFuncs
    malloc : YajlMallocFunc
    realloc : YajlReallocFunc
    free : YajlFreeFunc
    ctx : Void*
  end

  struct YajlCallbacks
    yajl_null : (Void* -> LibC::Int)
    yajl_boolean : (Void*, LibC::Int -> LibC::Int)
    yajl_integer : (Void*, LibC::LongLong -> LibC::Int)
    yajl_double : (Void*, LibC::Double -> LibC::Int)
    yajl_number : (Void*, LibC::Char*, LibC::Int -> LibC::Int)
    yajl_string : (Void*, UInt8*, LibC::Int -> LibC::Int)
    yajl_start_map : (Void* -> LibC::Int)
    yajl_map_key : (Void*, UInt8*, LibC::Int -> LibC::Int)
    yajl_end_map : (Void* -> LibC::Int)
    yajl_start_array : (Void* -> LibC::Int)
    yajl_end_array : (Void* -> LibC::Int)
  end

  struct YajlValS
    type : YajlType
    u : YajlValSU
  end

  struct YajlValSUArray
    values : YajlVal*
    len : LibC::Int
  end

  struct YajlValSUNumber
    i : LibC::LongLong
    d : LibC::Double
    r : LibC::Char*
    flags : LibC::UInt
  end

  struct YajlValSUObject
    keys : LibC::Char**
    values : YajlVal*
    len : LibC::Int
  end

  type YajlGen = Void*
  type YajlHandle = Void*

  union YajlValSU
    string : LibC::Char*
    number : YajlValSUNumber
    object : YajlValSUObject
    array : YajlValSUArray
  end
end
```

LibC の stdin などを使えるために簡単なラッパを作ります。

- src/yajl.cr

```ruby
require "./lib_yajl"

@[Link(ldflags: "-lyajl")]
lib LibYajl
  $stderr : Void*
  $stdout : Void*
  $stdin : Void*

  fun fread(ptr : Void*, size : Int32, nmemb : Int32, stream : Void*) : Int32
  fun fprintf(stream : Void*, format : UInt8*, ...) : Int32
end

class Yajl
  NULL = nil
  alias YajlType = LibYajl::YajlType

  def yajl_is_string(v)
    v ? v.value.type == YajlType::YajlTString : false
  end

  def yajl_get_string(v)
    yajl_is_string(v) ? v.value.u.string : ""
  end

  forward_missing_to LibYajl
end
```

以下の C のサンプルを Crystal に移植します
- https://github.com/lloyd/yajl/blob/2.1.0/example/parse_config.c

- example/parse_config.cr

```ruby
require "yajl"

class YajlTest < Yajl
  def run : Int32
    fileData = uninitialized UInt8[65536]
    errbuf = uninitialized UInt8[1024]

    rd = fread(fileData.to_unsafe, 1, fileData.size - 1, stdin)

    node = yajl_tree_parse(fileData.to_unsafe, errbuf.to_unsafe, errbuf.size)

    unless node
      fprintf(stderr, "parse_error: ")
      return 1
    end

    path = ["Logging".to_unsafe, "timeFormat".to_unsafe]
    v = yajl_tree_get(node, path.to_unsafe, YajlType::YajlTString)
    if v
      printf("%s/%s: %s\n", path[0], path[1], yajl_get_string(v))
    else
      printf("no such node: %s/%s\n", path[0], path[1])
    end

    yajl_tree_free(node)

    0
  end
end

fun main(argc : Int32, argv : UInt8**) : Int32
  YajlTest.new.run
end
```

コンパイルします。

```
crystal build --no-debug --release example/parse_config.cr
```

実行します。

```
% echo '{"Logging":{"timeFormat":"utc"}}' | ./parse_config
Logging/timeFormat: utc

% echo '{"Logging":{"":"utc"}}' | ./parse_config 
no such node: Logging/timeFormat
```

リポジトリ

https://github.com/S-YOU/yajl.cr

[1]: https://crystal-lang.org/docs/syntax_and_semantics/c_bindings/