MySQLのUDFをGoで書く

前回、Go1.5を使ってC共有ライブラリを生成しました。

この時は、単純にsoをビルドしてpythonのREPLからエクスポートされた関数が実行できるかを見たのですが、このテクノロジーを使ってMySQLのUDFをGoで書いてみたのが今回です。 環境は、以下です。

$ cat /etc/redhat-release 
CentOS release 6.6 (Final)
$ go version
go version go1.5 linux/amd64
$ mysql
mysql> select version();
+-----------+
| version() |
+-----------+
| 5.1.73    |
+-----------+
1 row in set (0.00 sec)

コードは、以下です。 github.com



まずは、ビルドします。

$ go build -buildmode=c-shared -o main.so main.go

次に、生成されたライブラリを、mysqlプラグイン配置先にコピーします。

$ sudo cp -p main.so  /usr/lib64/mysql/plugin/

あとは、mysqlCLIからUDFを定義し、実行します。 いろいろ考慮は足りていないとは思いますが、OSのシェルコマンドを文字列として渡せば実行結果が返ってくる的なものになっています。

mysql> create function myexec returns string soname 'main.so';
Query OK, 0 rows affected (0.00 sec)

mysql> select myexec("hostname");
+---------------------------------+
| myexec("hostname")              |
+---------------------------------+
| vagrant-centos66.vagrantup.com
 |
+---------------------------------+
1 row in set (0.01 sec)


書いてみてですが、Goで書けるようになったこと自体は面白くて、いろいろ妄想が膨らみますが、 以下のように、cgoで頑張って型変換をこねくり回しているので、煩雑という感じが。。。

//export myexec
func myexec (initid *C.UDF_INIT, args *C.UDF_ARGS, result *C.char, length *C.ulong, is_null *C.char, error *C.char) *C.char { 
  out, err := exec.Command(C.GoString(*args.args)).Output()
  if err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
  result = C.CString(string(out))
  *length = C.ulong(utf8.RuneCountInString(C.GoString(result)))
  return result
}

前回のように、ここにさらにmrubyまで導入して、 CとGoとmrubyでUDFを書くこともやってみたいけど更に複雑化するだけかも。。

go-mrubyでmrubyをC共有ライブラリに突っ込む

Go1.5から、C共有ライブラリを生成するオプションが追加されたらしい。

そこで、Goにmrubyを組み込んでC共有ライブラリを作成し、使ってみた。 環境は、

まずは、go-mrubyをダウンロード。

go get -d github.com/mitchellh/go-mruby

続いて、リポジトリのreadme通りmrubyをビルド。(ここで独自にmrbgemを組み込みたい場合は、makeしないで自前でビルドしたほうがよい)

cd $GOPATH/src/github.com/mitchellh/go-mruby
make

次に、試しに以下のようなコードを任意の場所に用意。 LoadString()で、引数の文字列をmrubyのコードとして実行し戻り値を受け取っている。

// example.go
package main                                                                                                                                                                                                                      
                                                                                                                                                                                                                                  
import ( 
  "C"                                                                                                                                                                                                                     
  "github.com/mitchellh/go-mruby"                                                                                                                                                                                                 
)                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                  
//export example                                                                                                                                                                                                                      
func example() {                                                                                                                                                                                                                      
  mrb := mruby.NewMrb()                                                                                                                                                                                                           
  defer mrb.Close()                                                                                                                                                                                                               
                                                                                                                                                                                                                                  
  _, err := mrb.LoadString(`5.times {|num| puts "*" * num}`)                                                                                                                                                                      
  if err != nil {                                                                                                                                                                                                                 
  ¦ panic(err.Error())                                                                                                                                                                                                            
  }                                                                                                                                                                                                                               
}                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                  
func main() {                                                                                                                                                                                                                     
} 

上記example.goと同ディレクトリに、最初に生成しておいた静的ライブラリ(libmruby.a)を置くことも忘れない。

cp -p $GOPATH/src/github.com/mitchellh/go-mruby/libmruby.a /path/to/example

最後に、以下でsoが生成されるぞお!

go build -buildmode=c-shared -o example.so example.go

と思ったら、エラーが発生した。

# command-line-arguments
/path/to/linker: running clang failed: exit status 1
clang: error: no such file or directory: 'libmruby.a'

いろいろ試してみたけど、これは、 go-mrubyのmruby.go内で、cgoのインポート時にLDFLAGSに指定しているlibmruby.aが見つからないと言っているので、

とりあえず、以下のようにmruby.go自体を書き換えてしまう。

@@ -3,7 +3,7 @@ package mruby
 import "unsafe"
 
 // #cgo CFLAGS: -Ivendor/mruby/include
-// #cgo LDFLAGS: libmruby.a -lm
+// #cgo LDFLAGS: /full/path/to/libmruby.a -lm
 // #include <stdlib.h>
 // #include "gomruby.h"
 import "C"

再度ビルドして、example.soが生成された!

example()がエクスポートされているかを確認する。よさそう。

$ nm example.so | grep -i example                    
0000000000071a90 T __cgoexp_a4c9f6073c4b_example
00000000000026e0 T _example
0000000000071ad0 t main.example
00000000001fc400 s main.example.f

ということで、例によってpythonのREPLでexample()を実行してみる。出た!

>>> import ctypes
>>> lib = ctypes.CDLL("./example.so")
>>> lib.example()

*
**
***
****
1



以上、非常に手軽にgoでC共有ライブラリを書けて、実は中身はmrubyでした!! ということができました。

次に、LoadString()でmrubyを読みこむ部分を、ファイルから読めるようにすると良いかなと思う。 そうすればリビルドすることなく挙動をコントロールできるという恩恵が得られます。

それは次回のエントリにしよう。。。

初Pull Requestがmergeされた

さきほど、自分のGitHubページを見ていたら、先日送信した初プルリクがマージされたことがわかった。

github.com

自分のアカウントがcontributorに並んだ!

と思って眺めていたら、あのmattnさんもcontributorに入っていた。 嬉しいなあ。

MySQLのCLIでコマンドヒストリをpeco検索・実行

pecoでシェルのコマンド履歴を検索・実行することが多いです。 書き捨てのワンライナーや滅多に叩かないコマンドなどでも、とりあえず一度でも実行していれば、 曖昧な記憶を頼りに再実行することができるからです。

これを、mysqlcliでも同様のことができないかと思いました。 管理系のコマンドが全く覚えられないのはもとより、書き捨ての簡単なクエリも後から再実行できたら楽かなと。。

少し調べてみても、似たような話題にヒットしなかったので、 取り急ぎ以下のような感じでやっています。

まず、以下の2つのスクリプトを用意します。

mysql_peco.sql

system /path/to/mysql_peco.sh
source /path/to/tmp.sql

mysql_peco.sh

#!/bin/zsh                                                                                                                                                                                                                    
                                                                                                                                                                                                                               
BUFFER=$(sed -e "s/\\040/ /g" $MYSQL_HISTFILE | sed -e 's/\\//g' | egrep ";$" | egrep -i "^select|^update|^insert|^show|^commit|^use|^pager|^desc" | awk '!a[$0]++' | peco) 
echo $BUFFER > /path/to/tmp.sql 

あとは、mysqlcliから上記mysql_peco.sqlをsourceするだけ。 動きとしては、履歴ファイルをpecoして一旦一時スクリプト(tmp.sql)に保存して、最後に一時スクリプトをsourceしているだけですが!

これで、記憶力が悪い自分でも、さくさくオペレーションできるようになったはず。。 egrepの部分は思いつくまま書いたのでカスタマイズしていこうと思います。

なお、以下のように.editrcにショートカットを定義しておくと、さらに楽。

.editrc

 mysql:bind -s "^R" "source /path/to/mysql_peco.sql;"

vagrant-global-statusをmrubyで書いた

前提

vagrant にはもともとglobal-statusオプションがあり、以下のように仮想マシンの状態をリストすることができます。

$ vagrant global-status
id       name    provider   state    directory                                       
-------------------------------------------------------------------------------------
941f568  default virtualbox aborted  /Users/juchino/my_vagrant/centos64_oracle
41ed4ec  default virtualbox aborted  /Users/juchino/my_vagrant/mruby_target          
9e4777a  default virtualbox aborted  /Users/juchino/my_vagrant/mruby_host            
00dcd43  default virtualbox poweroff /Users/juchino/my_vagrant/redmine2_5_2          
fc42378  core-01 virtualbox poweroff /Users/juchino/my_vagrant/coreos/coreos-vagrant 
121afc9  default virtualbox aborted  /Users/juchino/my_vagrant/centos56_mruby

・・・

ただ、Rubyの処理系を経由するため、結果が返ってくるまで結構待ちます。 自分のMac Book Airで計測すると、

$ time vagrant global-status
・・・
vagrant global-status  1.33s user 0.26s system 83% cpu 1.908 total

で、この処理速度を改善するため、Goで書き直されたのが、以下です。

github.com

このコマンドの処理速度は、自分のMac Book Airで計測すると、

$ time vagrant-global-status
・・・
vagrant-global-status  0.00s user 0.01s system 71% cpu 0.012 total

でした。 ちなみに、作者さんの環境での測定結果等は以下に載っております。

blog.monochromegane.com

やったこと

このvagrant-global-statusを、mrubyで書いてCの中に組み込んでみました。

github.com

mrubyのコードのほうで大半のことをしています。 $HOME/.vagrant.d/data/machine-index/index をパースして、 必要情報(マシンのid, マシン名, プロバイダ, 状態, ファイルシステム上のパス)のみArrayにどんどんpushしていきます。 mrubyからCに渡すのは、このArrayとなります。

class Vagrant

    attr_accessor :machines

    def initialize()
       @path = ENV['HOME'] + "/.vagrant.d/data/machine-index/index"
       @machines = Array.new()
    end

    def get_machines_status()
        file = File.open(@path)
        text = file.read
        file.close()

        JSON::parse(text)["machines"].each do |machine|
            @machines.push(machine[0].slice(0...7) + " " + sprintf("%-10s", machine[1]["name"].slice(0...10)) + " " + " " + sprintf("%-10s", machine[1]["provider"].slice(0...10)) + " " + sprintf("%-10s", machine[1]["state"].slice(0...10)) + " " + machine[1]["local_data_path"])
        end
    end
end

Cのほうでは、mrubyから受けとったArrayを表示しています。

    mrb_value res = mrb_funcall(mrb, mrb_top_self(mrb), "main", 0);

    for(int i = 0; i < RARRAY_LEN(res); i++){
        char *a = mrb_str_to_cstr(mrb, mrb_ary_ref(mrb, res, i)); 
        printf("%s\n", a);
    }

ビルドの方法は以下の通り。

まず、vagrant-global-status.rbをC言語配列形式のバイトコードにします。

mrbc -Bvagrant vagrant-global-status.rb

これで、以下のようなCのコード(vagrant-global-status.c)ができます。

 /* dumped in little endian order. 
   ¦use `mrbc -E` option for big endian CPU. */
#include <stdint.h>
const uint8_t
#if defined __GNUC__
__attribute__((aligned(4)))
#elif defined _MSC_VER
__declspec(align(4))
#endif
vagrant[] = {
0x45,0x54,0x49,0x52,0x30,0x30,0x30,0x33,0x94,0xaf,0x00,0x00,0x04,0x7b,0x4d,0x41, 
・・・
};

そして、上記を読み込むコードの方では以下のように書きます。

#include <stdio.h>
#include <string.h>
#include "mruby.h"
#include "mruby/compile.h"
#include "mruby/string.h"
#include "mruby/array.h"
#include "mruby/dump.h"
#include "vagrant-global-status.c"

int main (int argc, char *argv[]) {

    extern const uint8_t vagrant[];

    mrb_state* mrb = mrb_open();
    mrb_load_irep(mrb, vagrant);     # ここでバイトコードを読み込んでいる

    mrb_value res = mrb_funcall(mrb, mrb_top_self(mrb), "main", 0);

    for(int i = 0; i < RARRAY_LEN(res); i++){
        char *a = mrb_str_to_cstr(mrb, mrb_ary_ref(mrb, res, i)); 
        printf("%s\n", a);
    }

    mrb_close(mrb);

    return 0; 
}

以上のようにしてできたコード(main.c)をビルドすれば、1つのバイナリでglobal-statusができます!

測定!!

作成したmruby版vagrant-global-statusの速度を見てみました。

vagrant-global-status  0.00s user 0.00s system 65% cpu 0.011 total

Go版と大差ありません!でした。

ちなみに、今回使ったmrubyは、以下のモジュールを組み込む必要があります。

conf.gem :git => 'https://github.com/iij/mruby-io' 
conf.gem :git => 'https://github.com/iij/mruby-dir'
conf.gem :git => 'https://github.com/iij/mruby-env'
conf.gem :git => 'https://github.com/mattn/mruby-json'

mruby 触り始めました

最近、mrubyを触り始めています。 自分用に最初に作ったのは、SQLite(Google Chromeの閲覧履歴データ)からselectするmrubyのコードをCに組み込んでみたものです。

成果物

github.com

取り急ぎOSXの動作しか考慮しておりません。。
以下のようにビルドするとバイナリができます。

gcc -o gchist -I../path/to/mruby/mrbgems -I../path/to/mruby/include -I/usr/include gchist.c -lsqlite3 ../path/to/mruby/build/host/lib/libmruby.a

動作としては、以下のように実行すると、Google Chromeの閲覧履歴からタイトルのリストを標準出力に出すだけ。

$ ./gchist 
Google
GAE で Perl を動かせるなら - あくる日
QuickTime でサポートされるメディア形式を拡充する - Apple サポート
QuickTime でサポートされるメディア形式を拡充する - Apple サポート
QuickTime でサポートされるメディア形式を拡充する - Apple サポート
Play Windows Media on your Mac | Telestream Flip4Mac | Overview
Play Windows Media on your Mac | Telestream Flip4Mac | Overview
firefox chrome - Google 検索
・・・
・・・

または、引数を指定すると、SQL文のLIKEのようにして、タイトルの文字列とマッチするものだけを表示します。

$ ./gchist centos
Apache HTTP Server Test Page powered by CentOS
番外 VirtualBoxにインストールしたCentOS6の画面解像度を800×600以上に設定する方法 | Web 覚書ノート
【CentOS 5.5】Grubの設定ファイルが見つからない【カーネルパニック】 - Yahoo!知恵袋
ubuntu上のCentOSを再起動したときにカーネルパニックが発生する場合の対処 - think-t の晴耕雨読
Virtual PC 2007の導入記録(CentOS4.6) - 走り続けたい社内SEブログ
[CentOS] Docker の Remote API を HTTP 経由で使えるようにする - Qiita
Apache HTTP Server Test Page powered by CentOS

参考情報

  • mrubyのビルド

blog.matsumoto-r.jp

非常にわかりやすくて、とても助かります!!

  • 機能のアイディア

以下のアイディアを参考にさせていただき、CLIChromeの履歴を一覧表示する機能を作成しました。

github.com

日経Linuxを立ち読みしていた時に知ったプロダクトです。

日経Linux - 本誌目次 - 2015年3月号:ITpro

mruby_config

SQLiteを使うので、

mgem add mruby-sqlite3

をしておきます。

いろいろなmgemを試していた名残で、不要なものもたくさん入ってしまっていますが、現在のbuild_config.rbは以下のよう。

MRuby::Build.new do |conf|
  toolchain :gcc

  conf.bins = %w(mrbc)

  # mruby's Core GEMs
  conf.gem 'mrbgems/mruby-bin-mirb'
  conf.gem 'mrbgems/mruby-bin-mruby'
  conf.gem 'mrbgems/mruby-array-ext'
  conf.gem 'mrbgems/mruby-enum-ext'
  conf.gem 'mrbgems/mruby-eval'
  conf.gem 'mrbgems/mruby-exit'
  conf.gem 'mrbgems/mruby-fiber'
  conf.gem 'mrbgems/mruby-hash-ext'
  conf.gem 'mrbgems/mruby-math'
  conf.gem 'mrbgems/mruby-numeric-ext'
  conf.gem 'mrbgems/mruby-object-ext'
  conf.gem 'mrbgems/mruby-objectspace'
  conf.gem 'mrbgems/mruby-print'
  conf.gem 'mrbgems/mruby-proc-ext'
  conf.gem 'mrbgems/mruby-random'
  conf.gem 'mrbgems/mruby-range-ext'
  conf.gem 'mrbgems/mruby-sprintf'
  conf.gem 'mrbgems/mruby-string-ext'
  conf.gem 'mrbgems/mruby-string-utf8'
  conf.gem 'mrbgems/mruby-struct'
  conf.gem 'mrbgems/mruby-symbol-ext'
  conf.gem 'mrbgems/mruby-time'
  conf.gem 'mrbgems/mruby-toplevel-ext'

  # user-defined GEMs
  conf.gem :git => 'https://github.com/iij/mruby-io'
  conf.gem :git => 'https://github.com/iij/mruby-dir'
  conf.gem :git => 'https://github.com/iij/mruby-pack'
  conf.gem :git => 'https://github.com/iij/mruby-socket'
  conf.gem :git => 'https://github.com/matsumoto-r/mruby-simplehttp'
  conf.gem :git => 'https://github.com/matsumoto-r/mruby-httprequest'
  conf.gem :git => 'https://github.com/mattn/mruby-json'
  conf.gem :git => 'https://github.com/mattn/mruby-http'
  conf.gem :git => 'https://github.com/matsumoto-r/mruby-mrbgem-template'
  conf.gem :git => 'https://github.com/matsumoto-r/mruby-sleep.git'
  conf.gem :git => 'https://github.com/mattn/mruby-sqlite3.git'
  load '/Users/juchino/myruby/src/ujun/mruby_cross_compiler/mruby-cross-compile-on-mac-osx/mrbgem.rake'
end

雑感

mruby自体は、組み込み機器の文脈で話題になることが多いように思いますが、私の場合は、ちょっと目的が違います。

私の場合、制約があってRubyの処理系を導入できないような環境だけど、Rubyツール書きたい!となることが結構あります。このようなときに、軽いバイナリ一個持って来ればrubyでいろいろ書けてしまうmrubyは、非常に有難いです。

せっかく触り始めたのだし、今後業務で活用していければと思います。

コマンド履歴をスリム化してpecoでインクリメンタルサーチするとき見やすく。

最近、職場でもプライベートでも、 これみたいな感じでpecoを使ってコマンド履歴ベースでいろいろ作業してる。 これが生命線、これがないと生きられない。

Zsh - pecoを使って、コマンド履歴をインクリメンタルに絞り込んでコマンド再実行 - Qiita

ただ、↓みたいに大量にllが残ってしまっているのでなんかなあと思っていた。

QUERY>ll                                          
ll
ll
ll
mll
ll
ll
ll
ll
vim hello_world.rb
ll
ll
ll
ll
bundle install --path verndor/bundle
jruby -S bundle install --path vendor/bundle

とりあえずは以下で、.zsh_historyのコマンドが重複しているところを削除。 (ちなみに、履歴系のシェルのオプションはzshのデフォルトのまま)

#!/usr/bin/env perl

my %hash = (); 
 
while(<>) {
  print unless $hash{(split ";", $_)[1]}++;
}

重複コマンドを排除するような設定などあるみたいなので、適用してみようかな。

そういえば、.zsh_historyを変更しても、ログインし直さないとhistoryコマンドの結果には反映されなかったな。 historyのキャッシュをリフレッシュする方法がないか探してみよう。

それにしても、自分はなんか考え事するときとか無意識にll連打してるみたいだな。