GoでShared Libraryをビルドしてみた(簡単ドキュメント指向DB)

IoTみたいな小さなアプリケーションで、sqliteを使うほどでも無いけどクエリは動いてほしい、みたいなことがあると思う。
こういう用途に対しては、golangで簡単なドキュメント指向データベースをつくって、pythonとかから呼び出せるようにしてやれば便利そうだ。というわけでこの機会にちょっと試作してみた。

# 基本方針
 * goleveldbをバックエンドに使った適当なドキュメント指向DBっぽいもの
   * https://github.com/syndtr/goleveldb
 * Shared Libraryとして吐き出して他の言語から使えるようにする

# データベースの実装
適当に実装を練った。 テスト用というかイメージをつかむためにつくったので、極めて軽くつくった実装
＃ これをドキュメントDBと言ってよいかどうかは悩ましい

```testdb.go
package docdb

import (
        "bytes"
        "encoding/json"
        "fmt"
        "github.com/syndtr/goleveldb/leveldb"
        "github.com/syndtr/goleveldb/leveldb/util"
        "reflect"
        "strconv"
)

type Bucket struct {
        parent *DB
        name   []byte
}

func (b *Bucket) Store(doc []byte) error {
        val := map[string]interface{}{}
        err := json.Unmarshal(doc, &val)
        if err != nil {
                return fmt.Errorf("invalid document type")
        }

        idval, ok := val["id"]
        if !ok {
                return fmt.Errorf("property 'id' is not found")
        }
        id, err := b.generateLevelDbID(idval)
        if err != nil {
                return err
        }
        err = b.parent.db.Put(id, doc, nil)
        if err != nil {
                return err
        }
        return nil
}

func (b *Bucket) getByStringID(id interface{}) ([]byte, error) {
        lid, err := b.generateLevelDbID(id)
        if err != nil {
                return nil, err
        }

        dat, err := b.parent.db.Get(lid, nil)
        if err != nil {
                return nil, err
        }
        return dat, nil
}

func (b *Bucket) GetByQuery(query []byte) ([]byte, error) {
        it := b.parent.db.NewIterator(util.BytesPrefix(b.name), nil)
        defer it.Release()
        
        queryobj := map[string]interface{}{}
        err := json.Unmarshal(query, &queryobj)
        if err != nil {
                return nil, err
        }

dbiterator:
        for it.Next() {
                obj := map[string]interface{}{}
                err = json.Unmarshal(it.Value(), &obj)
                if err != nil {
                        continue
                }

                for qk, qv := range queryobj {
                        if v, ok := obj[qk]; ok {
                                if !reflect.DeepEqual(qv, v) {
                                        continue dbiterator
                                }
                 }
                return it.Value(), nil
        }
        return nil, nil
}

func (b *Bucket) generateLevelDbID(id interface{}) ([]byte, error) {
        idstr := ""
        switch t := id.(type) {
        case int:
                idstr = strconv.Itoa(t)
        case string:
                idstr = t
        default:
                return nil, fmt.Errorf("invalid ID type")
        }
        return bytes.Join([][]byte{b.name, {0x2d}, []byte(idstr)}, nil), nil
}

type DB struct {
        db *leveldb.DB
}

func OpenDB(path string) (*DB, error) {
        bdb, err := leveldb.OpenFile(path, nil)
        if err != nil {
                return nil, err
        }
        return &DB{
                db: bdb,
        }, nil
}

func (db *DB) GetBucket(name string) *Bucket {
        return &Bucket{
                parent: db,
                name:   []byte(name),
        }
}

func (db *DB) Close() error {
        return db.db.Close()
}
```

# Shared Libraryとして吐き出す
go 1.5からshared libraryを吐けるようになったので、このライブラリを他の言語からでも使ってやれる。
この時、パッケージはmainでなければならないらしい。``//export``から始まるコメントでCから関数を使えるようにするのは普通のcgoと同様の仕組みっぽい。

```lib.go
package main

import (
        "C"
        "github.com/umisama/docdb/docdb"
        "unsafe"
)

func main() {}

//export open
func open(path string, db unsafe.Pointer) int {
        var err error
        adb, err := docdb.OpenDB(path, true)
        if err != nil {
                return -1
        }
        db = unsafe.Pointer(adb)
        return 0
}

//export get
func get(db unsafe.Pointer, bucket, query, data string) int {
        adb := (*docdb.DB)(db)
        data, err := adb.GetBucket(bucket).GetByQuery([]byte(query))
        if err != nil {
                return -1
        }
        return 0
}

//export put
func put(db unsafe.Pointer, bucket, data string) int {
        adb := (*docdb.DB)(db)
        err := adb.GetBucket(bucket).Store([]byte(data))
        if err != nil {
                return -1
        }
        return 0
}
```

これを、``-build-mode=c-shared``を付けてビルドする。これでopen()、put()、get()の3つの関数を持ったlibraryを生成できる。

```
 $ go build -buildmode=c-shared -o docdb.so
```

# 問題点
外部からこのshared libraryを呼び出すとき、Goの世界のstringは*charではなくて、GoStringという型になる。これはlibraryのビルド時に同時に生成されるhファイルで以下のように定義される。

```c
typedef struct { char *p; GoInt n; } GoString;
```

これが(特にLL言語から)呼び出すときに面倒な手続きになる。これを書くくらいなら別にsqliteでも･･･とかちょっと思ってしまった。
解決策が無いか、今後検討していきたい。

## 追記
ライブラリ側で*C.char型で受ければ良いらしい。

```go
func open(path *C.char, db unsafe.Pointer) int {
        var err error
        goPath := C.GoString(path)
        adb, err := docdb.OpenDB(path, true)
～～～以下略～～
```