エンジニアと思われるものの備忘録

しがない学生の備忘録です

2018年振り返り & 2019年で頑張ること & 退職エントリー

振り返りと豊富とお世話になったインターン先の退職エントリーをひとまとめにしました。

2018年

1月 卒業論文で死にかけてた 2月 卒業論文で略 & インターン復帰した 3月 インターンでバリバリ開発してた 4月 同上 & 授業始まった & GoConにLTで参加した 5月 同上 6月 同上 7月 そろそろ授業に対してフラストレーションが溜まってきた 8月 インターン 9月 アドテクコンペとインターン 10月 内定決まった 11月 内定承諾した 12月 インターン退職した

研究の方は新しいテーマをずっと探していて調査しては探し直して調査しては探し直してを繰り返して未だに決まりきってはいません。 でも沢山論文は読んだし一個一個の読むスピードや単語の意味も大分わかってきたので無駄ではないと思っています。

インターンの方は1年9ヶ月もお世話になった3-shakeを退職しました。 3-shakeでは最初は機械学習のR&Dエンジニアとして入りましたがいつの間にかReckoner-CDPのバックエンドエンジニアとして集計基盤やサーバサイド、フロントエンドなんかもやったりしてました。おかげで1年9ヶ月でWeb開発の知識、スキルはかなり跳ね上がった気がします。

本当に3-shakeの皆さんにはお世話になりました

次どこいくの

CyberAgentさんのどこかのプロジェクトに内定者インターンとして関わる可能性が高いです。 つまり渋谷に通うことになるのですがあの人混みには未だに慣れません。豊洲の綺麗さと交換してほしい。

2019年はどうするの

とりあえずソフトウェアエンジニアとしてもっとスキルを磨きたいです。具体的にはCコンパイラの完成、アセンブラ、リンカの自作、AtCoderの継続参加でしょうか。 AtCoderは土日なかなか予定や他にやることが多くて時間を取れなかったので過去問解くだけでも毎週するようにしておきたいです。

あとは英語です。DMM英会話を申込もうと思ったのですがお金がないのでPodCastスクリプト見ながら発音して練習するというのを続けてみようと思います。

そんなわけで2019年もよろしくお願いします。

Goのプログラムで動的アップデートについてのトリビア

この記事は 

Go3 Advent Calendar 2018 - Qiita の21日目の記事にあたります。

アドベントカレンダーに参加するのは初めてのため、作法なので間違っているところがあったらすいません。

動的アップデート

動的アップデートという言葉をご存知でしょうか?ホットリロードだったりモンキーパッチだったり似たような意味で色々言い方があるかもしれませんがACMIEEEの論文を眺めているとDSU(Dynamic Software Update)と書いてることが多いです。

何をするかというと プログラムを中断させずにプログラムのアップデートを行う ことです。

具体的にはプロセスのメモリ空間を外部から書き換えて挙動を書き換えます。linuxにはlivepatchというツールがありますが、これはプロセスを指定し、アドレスを指定するとそのアドレスの値を書き換えることができます。要は黒魔術の一種です。

今回はGoで書かれたプログラムをGoでパッチ(厳密には違います)を書いて動的にプログラムの挙動を書き換える ということができるのかというのを検証してみます。

一般的な動的アップデートの手法

よく知られるDSUの手法を図に示します。

f:id:kk_river108:20181222003712p:plain

よくあるのが関数呼び出しで本来とは別の関数を呼び出すものです。

livepatchでは関数を呼び出したあと、直後のメモリ上の命令をjmp命令に書き換えてアップデート対象の関数アドレスに飛びます。 今回はこれを行います。

今回やってみるアップデートのフロー

  1. 自身を動的に書き換えるgoのプログラムを書く(アップデート対象のプログラム: target)
  2. 新しい関数を定義しただけのプログラムを書いてコンパイルする(パッチ: patch)
  3. targetはpatchのバイナリからアップデート対象となる関数のバイナリを抽出し、自身で呼び出している関数を書き変える

前提条件

  • 今回の記事における実験は全てgoコンパイラの最適化フラグはオフにしています。
  • os: ubuntu
  • go: 1.11
  • 関数の型はアップデート前とアップデート後で一致している

実装

以上の手順を行うコードを見ていきます。

target.go
package main

import (
    "fmt"
    "log"

    "github.com/Bo0km4n/lupus"
)

func f1() int {
    return 1
}

func main() {
    targetFunc := f1
    if err := lupus.PatchFunction(
        "/home/vagrant/go/src/github.com/Bo0km4n/lupus/example/patch",
        "main.f2",
        targetFunc,
    ); err != nil {
        log.Fatal(err)
    }
    fmt.Println(targetFunc())
}
patch.go
package main

func f2() int {
    return 2
}

func main() {
    f2()
}

targetのプログラムは普通なら1が表示されますが、patchを当てることにより2が表示されます。

f2のアセンブラはこのようになります

00000000004ce1c0 <main.f2>:
  4ce1c0:   48 c7 44 24 08 01 00    movq   $0x2,0x8(%rsp)
  4ce1c7:   00 00 
  4ce1c9:   c3                                       retq   

このバイト配列をうまいこと抽出して書き換えてあげればokです。あとこのアセンブラは後ほど関わってきますので少しだけ覚えていおいてください。

lupus.PatchFunction はgoのmonkeypatchライブラリを参考に関数の具体的な置き換えを行います。

PatchFunction

アップデートのロジックを順に追っていきます。

func PatchFunction(patchObjPath string, funcName string, target interface{}) error {
    elfFile, _ := open(patchObjPath)
    newFuncBytes := getFuncBytes(elfFile, funcName)
    newFuncBytes, err := writeFuncVal(newFuncBytes)
    if err != nil {
        return err
    }
    patchValue(reflect.ValueOf(target), newFuncBytes)
    return nil
}

まず、引数でもらったバイナリファイルを開いてElfファイルのセクションヘッダーを取れるようパースします。 open関数の中では goの debug/elf パッケージを用いてファイルを開いています。

getFuncBytes でセクションヘッダとシンボルテーブルからバイナリ内での対象の関数のアドレスを算出して関数の実体となるバイト配列を返しています。

writeFuncVal の内部でバイト配列をtmpファイルに書き出してファイルポインタをmmapシステムコールに渡してヒープ領域(多分)に確保します。 ここでnewFuncBytesを再代入しているのはwriteFuncVal実行後に返すバイト配列は中身は一緒ですがポインタが新しく確保したメモリのアドレスに書き換わっているからです。

つまり、関数の実行後にこの配列のアドレスにjmp命令でふっ飛ばせばokなわけです。

次の patchValue に新しい関数のバイト配列と対象となる関数を渡して実行するとメモリが書き換わります。

パッチのコア部分の実装

ではパッチでメモリを書き換える処理の実装について見てみます。

patchValue
func patchValue(target reflect.Value, replaceValue []byte) {
    if target.Kind() != reflect.Func {
        log.Fatal("Target has to be a Func")
    }
    replace(target.Pointer(), replaceValue)
}

ここで行うのはreflectパッケージで渡された関数ポインタがGoの関数であるかをチェックしているだけです。

replace
func replace(orig uintptr, replacement []byte) {
    replacePtr := *(*uintptr)(unsafe.Pointer(&replacement))
    jumpData := assembleJump(replacePtr)
    copyToLocation(orig, jumpData)
}

一行ずつ追っていきます。

replacePtrは先程確保したバイト配列のメモリ領域の先頭アドレスになります。

jumpDataは

func assembleJump(to uintptr) []byte {
    return []byte{
        0x48, 0xBA,
        byte(to),
        byte(to >> 8),
        byte(to >> 16),
        byte(to >> 24),
        byte(to >> 32),
        byte(to >> 40),
        byte(to >> 48),
        byte(to >> 56), // movabs rdx,to
        0xFF, 0xe2,     // jmp rdx
    }
}

このような処理を行い、

movabs rdx, to_address(バイト配列のアドレス)
jmp rdx

になる命令のバイト配列を示しています(ここはアーキテクチャ依存)。

最後にcopyToLocationでメモリ上の対象関数の命令を書き換えます。

func copyToLocation(location uintptr, data []byte) {
    f := rawMemoryAccess(location, len(data))

    page := rawMemoryAccess(pageStart(location), syscall.Getpagesize())
    err := syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)
    if err != nil {
        panic(err)
    }
    copy(f, data[:])
}

fはアップデート対象の関数が始まるアドレスから、パッチの長さ分のメモリ領域を取得します。 そして、対象のアドレスからpageサイズ分のアドレスを取得し、mprotectシステムコールを用いて4096バイト分(linuxのページサイズ)の権限を書き換えさせてもらいます。

これでメモリの書き換えと書き換え後の命令実行ができるようになりました。

最後にcopy関数で対象関数に入ったあと実行するはずだった命令の部分をjmp命令に書き換えて終わりです。

内部での関数呼び出し

今回のパッチは返す値を1から2にしただけです。 では次にこのような関数に処理を書き換えることにします。

func newFunc() int {
    fmt.Println("new func called")
    return 2
}

fmt.Printlnが追加されました。

これでパッチを当てようとしてみます。

すると。。。

unexpected fault address 0x7f0de5040fb0
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x1 addr=0x7f0de5040fb0 pc=0x7f0de5040fb0]

goroutine 1 [running]:
runtime.throw(0x4e4521, 0x5)
    /usr/lib/go-1.10/src/runtime/panic.go:616 +0x81 fp=0xc420041dc0 sp=0xc420041da0 pc=0x426ca1
runtime: unexpected return pc for runtime.sigpanic called from 0x7f0de5040fb0
stack: frame={sp:0xc420041dc0, fp:0xc420041e10} stack=[0xc420040000,0xc420042000)
000000c420041cc0:  000000000044a012 <runtime.writeErr+66>  0000000000000002
000000c420041cd0:  00000000004e4207  0000000000000001
000000c420041ce0:  0000000000000001  000000c420041d20
000000c420041cf0:  0000000000427758 <runtime.gwrite+280>  00000000004e4207
000000c420041d00:  0000000000000001  0000000000000001
000000c420041d10:  0000000000427758 <runtime.gwrite+280>  00000000004e4521
000000c420041d20:  000000c420041d70  0000000000427f0d <runtime.printstring+125>
000000c420041d30:  00000000004e4207  0000000000000001
000000c420041d40:  0000000000000001  00000000004e4207
000000c420041d50:  0000000000000001  00000000004e4207
000000c420041d60:  0000000000426bca <runtime.dopanic+74>  000000c420041d70
000000c420041d70:  000000000044baa0 <runtime.dopanic.func1+0>  000000c420000180
000000c420041d80:  0000000000426ca1 <runtime.throw+129>  000000c420041da0
000000c420041d90:  000000c420041db0  0000000000426ca1 <runtime.throw+129>
000000c420041da0:  0000000000000000  0000000000000005
000000c420041db0:  000000c420041e00  00000000004392f1 <runtime.sigpanic+529>
000000c420041dc0: <00000000004e4521  0000000000000005
000000c420041dd0:  000000c420041e10  00000000004a864c <github.com/Bo0km4n/lupus.patchValue+124>
000000c420041de0:  00007f0de5040fb0  000000c420000180
000000c420041df0:  0000000000001000  0000000000001000
000000c420041e00:  000000c420041e78 !00007f0de5040fb0
000000c420041e10: >00007f0de5061091  000000c420041e50
000000c420041e20:  0000000000000001  0000000000000001
000000c420041e30:  0000000000000013  00007f0de5061000
000000c420041e40:  0000000000001000  000000c420041e50
000000c420041e50:  00007f0de5074a20  00007f0de50b0440
000000c420041e60:  000000c420041e50  0000000000000001
000000c420041e70:  0000000000000001  000000c420041f78
000000c420041e80:  00000000004a9054 <main.main+356>  0000000000000000
000000c420041e90:  0000000000000047  00000000004e59cf
000000c420041ea0:  000000000000000c  00000000004bc0e0
000000c420041eb0:  00000000004ec218  0000000000000000
000000c420041ec0:  0000000000000000  000000c420041ef8
000000c420041ed0:  00000000004ec218  0000000000000010
000000c420041ee0:  00000000004c9040  00000000004ec218
000000c420041ef0:  000000c42000e320  0000000000000000
000000c420041f00:  0000000000000000  00000000004c9040
runtime.sigpanic()
    /usr/lib/go-1.10/src/runtime/signal_unix.go:395 +0x211 fp=0xc420041e10 sp=0xc420041dc0 pc=0x4392f1

見事にsegvりました。

結論から言うと今回の手法では 新しい関数内で関数を呼び出していた場合は無理 ということです。正確には無理ではないかもしれません

まず、アップデート後に呼び出すnewFuncのアセンブラを見てみます。

00000000004a9140 <main.newFunc>:
  4a9140:   64 48 8b 0c 25 f8 ff    mov    %fs:0xfffffffffffffff8,%rcx
  4a9147:   ff ff 
  4a9149:   48 3b 61 10              cmp    0x10(%rcx),%rsp
  4a914d:   0f 86 91 00 00 00     jbe    4a91e4 <main.newFunc+0xa4>
  4a9153:   48 83 ec 68           sub    $0x68,%rsp
  4a9157:   48 89 6c 24 60          mov    %rbp,0x60(%rsp)
  4a915c:   48 8d 6c 24 60           lea    0x60(%rsp),%rbp
  4a9161:   48 c7 44 24 70 00 00    movq   $0x0,0x70(%rsp)
  4a9168:   00 00 
  4a916a:   0f 57 c0                 xorps  %xmm0,%xmm0
  4a916d:   0f 11 44 24 38           movups %xmm0,0x38(%rsp)
  4a9172:   48 8d 44 24 38          lea    0x38(%rsp),%rax
  4a9177:   48 89 44 24 30         mov    %rax,0x30(%rsp)
  4a917c:   84 00                 test   %al,(%rax)
  4a917e:   48 8d 05 db 39 01 00     lea    0x139db(%rip),%rax        # 4bcb60 <type.*+0x12b60>
  4a9185:   48 89 44 24 38         mov    %rax,0x38(%rsp)
  4a918a:   48 8d 05 ef f3 04 00   lea    0x4f3ef(%rip),%rax        # 4f8580 <main.statictmp_1>
  4a9191:   48 89 44 24 40         mov    %rax,0x40(%rsp)
  4a9196:   48 8b 44 24 30          mov    0x30(%rsp),%rax
  4a919b:   84 00                 test   %al,(%rax)
  4a919d:   eb 00                   jmp    4a919f <main.newFunc+0x5f>
  4a919f:   48 89 44 24 48         mov    %rax,0x48(%rsp)
  4a91a4:   48 c7 44 24 50 01 00    movq   $0x1,0x50(%rsp)
  4a91ab:   00 00 
  4a91ad:   48 c7 44 24 58 01 00    movq   $0x1,0x58(%rsp)
  4a91b4:   00 00 
  4a91b6:   48 89 04 24              mov    %rax,(%rsp)
  4a91ba:   48 c7 44 24 08 01 00    movq   $0x1,0x8(%rsp)
  4a91c1:   00 00 
  4a91c3:   48 c7 44 24 10 01 00    movq   $0x1,0x10(%rsp)
  4a91ca:   00 00 
  4a91cc:   e8 1f ff fd ff          callq  4890f0 <fmt.Println>
  4a91d1:   48 c7 44 24 70 02 00    movq   $0x2,0x70(%rsp)
  4a91d8:   00 00 
  4a91da:   48 8b 6c 24 60           mov    0x60(%rsp),%rbp
  4a91df:   48 83 c4 68           add    $0x68,%rsp
  4a91e3:   c3                       retq   
  4a91e4:   e8 27 3d fa ff        callq  44cf10 <runtime.morestack_noctxt>
  4a91e9:   e9 52 ff ff ff         jmpq   4a9140 <main.newFunc>
  4a91ee:   cc                       int3   
  4a91ef:   cc                       int3   

先程見せたf1のアセンブラとはめちゃくちゃ違います。 ただfmt.Printlnを追加しただけなのに。

完全な憶測ですが、goのコンパイラ側がスタックの調整やGC関連の命令を埋め込んでいるのではないかと思います。 ただ埋め込むだけなのは良いですが、問題は jbe 4a91e4 <main.newFunc+0xa4> こんな感じの命令が散見されることです。

これらの命令は相対アドレスを参照しています。ここまで書けば あっ...(察し)ってなるでしょう。

当然先ほどの手法では上記のアセンブラをそのままヒープ領域に確保して書き込み実行します。 つまり jbe 4a91e4 <main.newFunc+0xa4> のようなアドレスをそのまま踏んでセグフォってしまうのです。

流石にこの辻褄あわせを行う気力もどうやれば上手くやれるかも思いつきませんでした。

まとめ

返す値をちょろっと変えるだけならパッチを当てることができましたが、関数呼び出しを加えるのは難しいことがわかりました。

今回の記事は研究の一貫で調べていたものが検証していく中でかなり難しいことだとわかり、一旦保留になったので供養の意味も込めて書かせていただきました。 動的アップデートはそこまでホットな研究テーマではありませんが低レイヤなシステムプログラミングやソフトウェア工学等の観点から議論されている面白いテーマでもあります。 よかったら調べてみてください。

今回の実験したコードは下記のリポジトリにあります。 そのうちREADMEに実行のしかたとか書き加えると思います。 github.com

参考文献

postd.cc

www.amazon.co.jp

Rustのモジュールシステムが厳格

goは1年半ぐらい結構書いてきて、シンタックスもそこまで複雑ではないので大分慣れてきた気がした。 なので最近はrustをちょくちょく書いてる。

今はgoで書いた8086のディスアセンブラをrustで少しずつ書き直そうと思ってる

github.com

クローラとかは若干飽きたため。monkeyインタプリタでもよかったけど、これは最近書いたばっかでしっかり記憶に残っちゃっているのでほぼ忘れたディスアセンブラにした。

で、rustはgoのパッケージシステムと似たようなモジュールシステムがある。というか書き方を見た感じC++とほぼ一緒なのだろうか。

rustのモジュールの切り方はgoのパッケージのように気楽に切ることはできない。なぜならimportに明確に親子関係が定められているから。

goの場合例えば下記のようなパッケージ構成のとき、

.
├── ast
│   ├── ast.go
│   └── ast_test.go
├── evaluator
│   ├── builtins.go
│   ├── evaluator.go
│   └── evaluator_test.go
├── lexer
│   ├── lexer.go
│   └── lexer_test.go
├── main.go
├── object
│   ├── environment.go
│   └── object.go
├── parser
│   ├── parser.go
│   ├── parser_test.go
│   └── parser_tracing.go
├── repl
│   └── repl.go
└── token
    └── token.go

当然だが、replパッケージからparser, astパッケージをimportできる。

しかし、rustのモジュールシステムの場合はそれができない。

例えばrustで以下のようにモジュールを切ってみる。

.
├── disassembler
│   ├── mod.rs
│   └── reader.rs
├── hoge
│   └── mod.rs
└── main.rs

disassemblerのreaderからhogeを参照しようとしてみる

コードは次のようになる

hoge/mod.rs

pub fn fuga() {
    println!("fuga");
}

disassembler/reader.rs

use std::vec::Vec;
mod hoge;
pub fn start(input: Vec<u8>) {
    hoge::fuga();
    let mut da: DisAssembler8086 = DisAssembler::new(input);
    println!("{:?}", da.body);
    da.dump_cur_token();
    da.next();
    da.dump_cur_token();
    while !da.is_end() {
        da.next();
    }
    da.dump_cur_token();
}
・・・
以下略

これをビルドしようとすると次のエラーが出る

error[E0583]: file not found for module `hoge`
 --> src/disassembler/reader.rs:2:5
  |
2 | mod hoge;
  |     ^^^^
  |
  = help: name the file either reader/hoge.rs or reader/hoge/mod.rs inside the directory "src/disassembler"

error: aborting due to previous error

For more information about this error, try `rustc --explain E0583`.

エラーの通り mod hoge とreader内で書くと src/disassembler/reader/hoge/mod.rssrc/disassembler/reader/hoge.rs を探そうとする。

つまり親ディレクトリを経由したmodの参照はできないということ(もし参照できる方法あったらすいません)。

これは同一階層でのディレクトリ同士では完全に実装が依存しないようになるので結構良い気がする。

まとめ

  • rustでモジュールを切るときは依存関係をちゃんと考えて切るようにしよう
  • 同一階層内で使いたい場合は同じディレクトリにファイルを分けるようにする

まだ全然知見がないけどgoとは全然違う厳しさがあるので面白い

Golangにおけるプロジェクトのレイアウトについて

僕がGolangを好きな理由はいくつかあるが、主なものは

  1. コードが皆フォーマッターによって大体綺麗になる
  2. 言語機能が充実しすぎていないので処理が追いやすい(コーディングする時不便に感じることもあるが、チームでの開発においては読まれる方を重視したほうが良い)
  3. サードパーティのライブラリの導入が非常に簡単(もちろんプロジェクトに導入するときは慎重に行うべき)

そんなGoだけど、プロジェクトを作る時にどうパッケージを切っていくか。これが以外と暗黙の了解みたいな感じになっている気がする。

APIであればMVCライクに切るか、最近だとクリーンアーキテクチャが自分の流行だ。

CLIツールやライブラリだとプロジェクトのルート直下にフラットにファイルが展開されているものもある。

ということで少し気になったのでGoでプロジェクトを作る時にデファクトとなるようなパッケージレイアウトはあるのか?もし、あるとしたらどういう風に切るのかを少し調べてみた。

まず golang project layout みたいな感じでググるとこのリポジトリが先頭にあがった。

GitHub - golang-standards/project-layout: Standard Go Project Layout

スターは4000近くもある。それなりに認知度が高いらしい。

READMEにはこのようにかかれている

これはGoアプリケーションプロジェクトの基本レイアウトです。 Goデベロッパーチームが定義した公式の標準ではありません。 ただ、このリポジトリの構成はGoエコシステムにおける共通の歴史的かつ新しいプロジェクトレイアウトパターンのセットです。 この切り方は、他のパターンよりも人気があります。 この構成ではいくつかの小さな拡張機能と、十分な大きさのプロダクションレベルでのアプリケーションに共通するいくつかのサポートディレクトリを備えています。

とのこと。確かにこの構成はよく見るような気がする。特にinternalパッケージとpkgパッケージは他のライブラリやツールでも頻繁に登場する。

READMEにはそれぞれこのように注釈されている

internal

内部でのみ使われるアプリケーションとライブラリコード。 これは、他の人のアプリケーションやライブラリでインポートさせたくないコードです。

/ internal / pkgディレクトリ(例:/ internal / pkg / myprivlib)に、実際のアプリケーションコードを書いて、/ internal / appディレクトリ(/ internal / app / myapp)で更に共有される。

共有の部分がいまいちピンと来ないけど、コアのコードやヘルパー的なコードはこの中で書こうということなのだろう。

pkg

外部アプリケーション(たとえば、/ pkg / mypubliclib)で使用できるライブラリコード。 他のプロジェクトは、pkg内のコードが動作することを期待してパッケージwpインポートするので、ここに何かを入れる前にしっかり考えてください:-)

また、ルートディレクトリに多くの非Goコンポーネントディレクトリが含まれていて、GopherCon EU 2018のインダストリアルプログラミングのベストプラクティスに記載されているように、さまざまなGoツールを簡単に実行できるように、Goコードを1か所にグループ化する方法です。

このプロジェクトレイアウトパターンを使用している人気のあるGoリポジトリを確認するには、/ pkgディレクトリを参照してください。 これは一般的なレイアウトパターンですが、普遍的に受け入れられているわけではなく、Goコミュニティの一部では推奨していません。

ふむふむ。基本的に外部で利用する時はこのpkgディレクトリが参照されることが多いということか。 試しにこの構成に心当たりのあるプロジェクトをいくつか挙げてみようと思う。

github.com

僕もよくお世話になっているkubernetesは今回のstandard repositoryに近い構成だ。internalパッケージはないが pkg, cmd, api等他のツールでもよく見るような構成になっている。

次は、、、といきたいとこだったんだけど、以外にもgo-awesome経由でリポジトリを漁っているとpkgディレクトリはあるけどinternalはない。みたいなケースが多くて探すのが疲れた。

go-awesomeに入っているツールやライブラリの構成を眺めていて思ったのはフラットな構成がかなり多いってことだ。勿論小さなプロジェクトであればそれでいいしわかりやすいから全然いいと思う。

他の大きなプロジェクトも名称等は違えど、役割として共通しているのだろうなっていうディレクトリが多かった気がする。

ちなみに英語であれば今回のプロジェクトのレイアウトについて論じているブログ等はある

medium.com

hackernoon.com

どっちも挙げている構成は最初に挙げたリポジトリと共通点が多い。ただ、Golangの公式からガイドライン等が発表されていないのであくまで全て非公式なものではある。 僕個人としては他のプロジェクトのコードを少し追いたい時などはこういう風に共通点があると非常に楽なので助かる。

まとめ

  • プロジェクトのレイアウトの切り方は大きなプロジェクトにおいては結構共通点が多い
  • でも明確にレイアウトのガイドラインが決定されているわけではないので好きにやればいい

Goのプロジェクト内でgRPCで自分で定義したprotoファイルをimportする

自分で作ったprotoファイルを他のprotoファイルから参照する時にハマったのでメモ

サンプルプロジェクトの構成

.
├── Makefile
├── team
│   └── handler
│       └── grpc
│           └── team
│               ├── team.pb.go
│               └── team.proto
└── user
    └── handler
        └── grpc
            └── user
                ├── user.pb.go
                └── user.proto

8 directories, 5 files

構成はクリーンアーキテクチャライクにしています

proto ファイル

userから

syntax = "proto3";

package user;


service UserAPI {
    rpc GetUsers(User) returns (Users) {}
    rpc Show(User) returns (User) {}
    rpc Delete(User) returns (Empty) {}
    rpc Invite(User) returns (Empty) {}
    rpc Activate(ActivateRequest) returns (ActivateResponse) {}
}

message User {
    int64 id = 1;
    int64 team_id = 2;
    string name = 3;
    string email = 4;
    string password = 5;
    bool isOwner = 8;
}



message ActivateRequest {
    string code = 1;
}

message ActivateResponse {
    string token = 1;
}

message Users {
    repeated User Users = 1;
}

message Empty {}

普通のprotoファイルです

次はteam

syntax = "proto3";
package team;
import "github.com/Bo0km4n/go-protobuf-ref-sample/user/handler/grpc/user/user.proto";
service TeamAPI {
}

message Team {
    int64 id = 1;
    string name = 2;
    user.Users users = 3;
}

message InviteRequest {
    string team = 1;
    string email = 2;
    string password = 3;
}

message ActivateRequest {
    string code = 1;
}

message ActivateResponse {
    string token = 1;
}

message Empty {}

userを内部で参照しています。

コンパイル

最初は以下のページを参考にコンパイルを行っていましたが生成されるpbファイルのimportパスが相対パスになっており go build 時に参照できません

qiita.com

そこで上記のように github.com/... のように通常のgoのimportパスと同様にすることでこの問題を解決します

ビルドに使用するmakefileをつぎのように書きました

PROTO_TARGETS = user team
PROJECT_PATH = github.com/Bo0km4n/go-protobuf-ref-sample 
proto: 
   @echo $(PROTO_TARGETS); \
      cd $$GOPATH/src; \
  for t in $(PROTO_TARGETS); do \
      echo "protobuf build ... $$t"; \
      protoc --go_out=plugins=grpc:. $(PROJECT_PATH)/$$t/handler/grpc/$$t/$$t.proto; \
  done

最初にechoしているのはしないと cd のせいなのか Nothing to done って言われるからです

上記のリンクではプロジェクトのルート以下のパスが生成されるimportパスになっていました。 つまり、$GOPATH/src で上記のビルドを行えば生成されるimportパスが正常になります。

実際に生成されたteamのpbファイルは次のようになります

// Code generated by protoc-gen-go. DO NOT EDIT.
// source: github.com/Bo0km4n/go-protobuf-ref-sample/team/handler/grpc/team/team.proto

package team

import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import user "github.com/Bo0km4n/go-protobuf-ref-sample/user/handler/grpc/user"

import (
    context "golang.org/x/net/context"
    grpc "google.golang.org/grpc"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package

type Team struct {
    Id                   int64      `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
    Name                 string     `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
    Users                *user.Users `protobuf:"bytes,3,opt,name=users" json:"users,omitempty"`
    XXX_NoUnkeyedLiteral struct{}   `json:"-"`
    XXX_unrecognized     []byte     `json:"-"`
    XXX_sizecache        int32      `json:"-"`
}

... 以下省略

これでプロジェクトのビルドも上手くいきます

$GOPATH/src に移動するのは少々スマートではない気がしますが色々調べて他に良いやり方がなかったので今回は妥協しました

一応今回のサンプルをgithubにあげておきます

github.com

Ubuntu14.04上でglibcをbuildしてリンクする

glibc内のソースをいじりたかったのでその前準備にglibcソースからビルドしてコードにリンクするまでの手順をメモ

環境

vagrant@vagrant-ubuntu-trusty-64:~/libc$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.5 LTS
Release:    14.04
Codename:   trusty

vagrant上で動かしてます

glibcの取得

$ sudo apt-get update
$ mkdir -p ~/libc/build # libcのビルド用ディレクトリを作成します。build用ディレクトリを分けないとconfigure時に怒られるため
$ cd ~/libc
$ sudo apt-get install -y dpkg-dev
$ sudo apt-get source libc6

終わったときの~/libcの中はこんな感じのはず

vagrant@vagrant-ubuntu-trusty-64:~/libc$ ll
total 13360
drwxrwxr-x  4 vagrant vagrant     4096 Oct 17 09:21 ./
drwxr-xr-x  6 vagrant vagrant     4096 Oct 17 09:54 ../
drwxrwxr-x 60 vagrant vagrant     4096 Oct 17 09:42 build/
drwxr-xr-x 73 root    root        4096 Oct 17 09:14 eglibc-2.19/
-rw-r--r--  1 root    root     1043124 Jan 17  2018 eglibc_2.19-0ubuntu6.14.debian.tar.xz
-rw-r--r--  1 root    root        7195 Jan 17  2018 eglibc_2.19-0ubuntu6.14.dsc
-rw-r--r--  1 root    root    12610800 Feb 21  2014 eglibc_2.19.orig.tar.xz

configureとmake

最終的に吐き出すディレクトリは /usr/eglibc-2.19 にします。これは好きなディレクトリで大丈夫です

$ sudo mkdir -p /usr/eglibc-2.19
$ ~/libc/build
$ ../eglibc-2.19/configure --prefix=/usr/eglibc-2.19 --enable-add-ons=nptl --enable-all-warnings
$ sudo make # かなり時間がかかります
$ sudo make install

以上で /usr/eglibc-2.19 以下にビルドされたファイル群が存在すれば大丈夫です

テスト用のコードとビルド

試しに以下のコードを書いてコンパイルしてみます

#include <stdio.h>
int main() {
  printf("hello\n");
  exit(0);
}

Makefileはこんな感じ

CC=gcc
CFLAGS = -nostdinc -I/usr/eglibc-2.19/include -I/usr/lib/gcc/x86_64-linux-gnu/4.8/include
LDFLAGS = -Wl,-rpath=/usr/eglibc-2.19/lib
TARGET = test.exe
OBJECTS = test.o

all: $(TARGET)

$(TARGET):$(OBJECTS)
        $(CC) $(LDFLAGS) $(OBJECTS) -o $(TARGET)

.c.o:
        $(CC) $(CFLAGS) -c -o $@ $<

clean:
        rm -f *.o $(TARGET)

CFLAGSとLDFLAGSに指定しているパスは各自がconfigure時に設定したprefixパスに合わせてください

makeして ldd コマンドで指定したglibcにリンクできているか確認します

vagrant@vagrant-ubuntu-trusty-64:~$ ldd test.exe
    linux-vdso.so.1 =>  (0x00007ffe58f8d000)
    libc.so.6 => /usr/eglibc-2.19/lib/libc.so.6 (0x00007f34057a3000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f3405b50000)

libc.so.6の部分が上のように指定したlibcになっていればokです

Gormにおける多層外部キーの定義の仕方とコードの書き方

最近バイトの方でgormの振る舞いとデータベースのテーブル構造のことでハマったのでそれについてメモっておきます

テーブル構造とgoのモデル

f:id:kk_river108:20180825134759p:plain

最初にあったテーブル構造は上の図のような感じ。

今回は例として地域、地域に存在する書店たち、書店の持つ本をマッピングしてみました。

地域と書店の関係はhas many、書店と本もhas manyとなっています。

これをgoのコードにmodelとして書き出すと以下のような感じになりました。

package main

import "time"

type Region struct {
    ID int
    Shops     []Shop `gorm:"foreignkey:ID"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

type Shop struct {
    ID int
    Name      string
    Books     []Book `gorm:"foreignkey:ShopID"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

type Book struct {
    ID        int
    ShopID    int
    Name      string
    Price     int
    CreatedAt time.Time
    UpdatedAt time.Time
}

テーブル作成のsqlは次のようになります

CREATE TABLE IF NOT EXISTS region (
        id                   serial  NOT NULL,
    created_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    updated_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    CONSTRAINT region_id PRIMARY KEY ( id )
);

CREATE TABLE IF NOT EXISTS shop (
        id                   serial  NOT NULL,
        name                 varchar(255) NOT NULL,
    created_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    updated_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    CONSTRAINT shop_id PRIMARY KEY ( id ),
        CONSTRAINT shop_id_region_id_foreign FOREIGN KEY ( id )
          REFERENCES region( id )
          ON UPDATE NO ACTION
          ON DELETE NO ACTION
);

CREATE TABLE IF NOT EXISTS book (
        id                   serial  NOT NULL,
        shop_id              bigint(20) unsigned NOT NULL,
        name                 varchar(255) NOT NULL,
        price                int NOT NULL,
        created_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    updated_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    CONSTRAINT book_id PRIMARY KEY ( id ),
        CONSTRAINT book_shop_id_shop_id_foreign FOREIGN KEY ( shop_id )
          REFERENCES shop( id )
          ON UPDATE NO ACTION
          ON DELETE NO ACTION
);

shopは自身のIDは外部キーでregionのidと紐づけています bookはshop idを外部キーでshopのidと紐づけています

gormでinsertする

これらのデータ構造はgormにおいて次のように一括でcreateすることができます。

      r := Region{
        Shops: []Shop{
            Shop{
                Name: "shop1",
                Books: []Book{
                    Book{
                        Name:  "book1",
                        Price: 100,
                    },
                    Book{
                        Name:  "book2",
                        Price: 200,
                    },
                },
            },
        },
    }

    if err := mysql.Create(&r).Error; err != nil {
        log.Fatal(err)
    }

gormがタグに紐づけた外部キーの情報からよしなに親をinsertした後、確定したIDの情報を用いて, 子のテーブルにまでinsertしてくれるのです。

しかし、このデータ構造の場合次のようなエラーが起こります。

/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:27:44]  [3.76ms]  INSERT INTO `region` (`created_at`,`updated_at`) VALUES ('2018-08-25 13:27:44','2018-08-25 13:27:44')
[1 rows affected or returned ]

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:27:44]  [10.12ms]  UPDATE `shop` SET `name` = 'shop1', `created_at` = '0001-01-01 00:00:00', `updated_at` = '2018-08-25 13:27:44'  WHERE `shop`.`id` = '2'
[0 rows affected or returned ]

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:27:44]  Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:27:44]  [5.02ms]  INSERT INTO `book` (`shop_id`,`name`,`price`,`created_at`,`updated_at`) VALUES ('2','book1','100','2018-08-25 13:27:44','2018-08-25 13:27:44')
[0 rows affected or returned ]

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:27:44]  Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:27:44]  Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:27:44]  Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)
2018/08/25 13:27:44 Error 1452: Cannot add or update a child row: a foreign key constraint fails (`debug`.`book`, CONSTRAINT `book_shop_id_shop_id_foreign` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)

エラーの内容はupdateが走り, それはconstraintに違反しているというものでした(多分)

これの原因はShopの主キーであり、外部キーでもあるIDとRegionの主キーを紐づけていることに起因すると思われます。

正しいテーブル構造

なので、ちゃんとしたテーブル構造は次のようになります

f:id:kk_river108:20180825140200p:plain

sqlはこんな感じ

CREATE TABLE IF NOT EXISTS region (
    id                   serial  NOT NULL,
    created_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    updated_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    CONSTRAINT region_id PRIMARY KEY ( id )
);

CREATE TABLE IF NOT EXISTS shop (
    id                   serial  NOT NULL,
    region_id            bigint(20) unsigned NOT NULL,
    name                 varchar(255) NOT NULL,
    created_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    updated_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    CONSTRAINT shop_id PRIMARY KEY ( id ),
    CONSTRAINT shop_region_id_region_id_foreign FOREIGN KEY ( region_id )
        REFERENCES region( id )
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
);

CREATE TABLE IF NOT EXISTS book (
    id                   serial  NOT NULL,
    shop_id              bigint(20) unsigned NOT NULL,
    name                 varchar(255) NOT NULL,
    price                int NOT NULL,
    created_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    updated_at           timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP  ,
    CONSTRAINT book_id PRIMARY KEY ( id ),
    CONSTRAINT book_shop_id_shop_id_foreign FOREIGN KEY ( shop_id )
        REFERENCES shop( id )
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
);

goのmodelのコードはこんな感じ

type Region struct {
    ID        int
    Shops     []Shop `gorm:"foreginkey:RegionID"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

type Shop struct {
    ID        int
    RegionID  int
    Name      string
    Books     []Book `gorm:"foreignkey:ShopID"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

type Book struct {
    ID        int
    ShopID    int
    Name      string
    Price     int
    CreatedAt time.Time
    UpdatedAt time.Time
}

ShopにRegionIDという外部キー用のカラムを追加しています。

これで先程のcreateを実行してみると

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:18:15]  [2.14ms]  INSERT INTO `region` (`created_at`,`updated_at`) VALUES ('2018-08-25 13:18:15','2018-08-25 13:18:15')
[1 rows affected or returned ]

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:18:15]  [6.41ms]  INSERT INTO `shop` (`region_id`,`name`,`created_at`,`updated_at`) VALUES ('2','shop1','2018-08-25 13:18:15','2018-08-25 13:18:15')
[1 rows affected or returned ]

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:18:15]  [1.57ms]  INSERT INTO `book` (`shop_id`,`name`,`price`,`created_at`,`updated_at`) VALUES ('1','book1','100','2018-08-25 13:18:15','2018-08-25 13:18:15')
[1 rows affected or returned ]

(/Users/bo0km4n/go/src/github.com/Bo0km4n/dev/mysql_gorm_association/main.go:32)
[2018-08-25 13:18:15]  [3.31ms]  INSERT INTO `book` (`shop_id`,`name`,`price`,`created_at`,`updated_at`) VALUES ('1','book2','200','2018-08-25 13:18:15','2018-08-25 13:18:15')
[1 rows affected or returned ]
main.Region{
  ID:    2,
  Shops: []main.Shop{
    main.Shop{
      ID:       1,
      RegionID: 2,
      Name:     "shop1",
      Books:    []main.Book{
        main.Book{
          ID:        1,
          ShopID:    1,
          Name:      "book1",
          Price:     100,
          CreatedAt: 2018-08-25 13:18:15 Local,
          UpdatedAt: 2018-08-25 13:18:15 Local,
        },
        main.Book{
          ID:        2,
          ShopID:    1,
          Name:      "book2",
          Price:     200,
          CreatedAt: 2018-08-25 13:18:15 Local,
          UpdatedAt: 2018-08-25 13:18:15 Local,
        },
      },
      CreatedAt: 2018-08-25 13:18:15 Local,
      UpdatedAt: 2018-08-25 13:18:15 Local,
    },
  },
  CreatedAt: 2018-08-25 13:18:15 Local,
  UpdatedAt: 2018-08-25 13:18:15 Local,
}

ちゃんと一括でinsertされているのがわかりました!

まとめ

そもそも最初失敗のケースでハマっていたのは親子関係のみの場合はうまく行っていたからです。親子孫のように増えると先程のようなエラーが出て四苦八苦していました。

ちなみに例に上げた親子の関係は1to1でもエラーは起きます。

テーブルの主キーと他のテーブルの主キーを外部キーとして紐付けるのは割とやりがちなケースな気がしますが、しっかり別カラムで紐づけた方が安全な気はします。

今回のサンプルコードは以下に載せてあります

github.com