LoginSignup
22
19

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-12-18

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

基本方針

  • goleveldbをバックエンドに使った適当なドキュメント指向DBっぽいもの
  • 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ファイルで以下のように定義される。

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

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

追記

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
19