雑なメモ書き

気楽にいきます

rustのgdbでのdebugでsourceを出力したかった

プログラム言語処理系のslackの#rustでKeenさんという方に色々教わってうまいこと動いたので書き残しておきます。

cargo buildをすると謎のdebug情報が付加される

  • /rustc/412f43ac5b4ae8c3599e71c6972112e9be4758faといったパスが情報に付加される
cargo build
nm -l target/debug/hello | grep /rustc
000000000000d5d0 T __rust_maybe_catch_panic  /rustc/412f43ac5b4ae8c3599e71c6972112e9be4758fa//src/libpanic_unwind/lib.rs:75
000000000000d660 T __rust_start_panic   /rustc/412f43ac5b4ae8c3599e71c6972112e9be4758fa//src/libpanic_unwind/lib.rs:95
000000000000be20 T rust_begin_unwind    /rustc/412f43ac5b4ae8c3599e71c6972112e9be4758fa//src/libstd/panicking.rs:301
000000000000d790 T rust_eh_personality  /rustc/412f43ac5b4ae8c3599e71c6972112e9be4758fa//src/libpanic_unwind/gcc.rs:288
000000000000b640 T rust_oom /rustc/412f43ac5b4ae8c3599e71c6972112e9be4758fa//src/libstd/alloc.rs:209

gdbにはset substitute-pathという実行時に変更出来る設定があるのでそれを利用するとsできる

  • gdbよく知らないけど簡単にshellかいてみる
#!/bin/sh

TARGET_PATH=$1
DEBUG_SRC=`strings $TARGET_PATH  | grep -o '^/rustc/[^/]\+/' | uniq`
TMPGDBINIT=`mktemp -d`
echo "set substitute-path $DEBUG_SRC $HOME/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/" >> $TMPDIR/.gdbinit
rust-gdb -x $TMPDIR/.gdbinit $TARGET_PATH
(gdb) l
1   fn main() {
2       println!("Hello, world!");
3   }
(gdb) b 2
Breakpoint 1 at 0x3f70: file src/main.rs, line 2.
(gdb) run
Starting program: /data/target/debug/hello
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, hello::main () at src/main.rs:2
2       println!("Hello, world!");
(gdb) s
core::fmt::Arguments::new_v1 (pieces=&[&str](len: 1) = {...}, args=&[core::fmt::ArgumentV1](len: 0))
    at /rustc/412f43ac5b4ae8c3599e71c6972112e9be4758fa/src/libcore/fmt/mod.rs:319
319             args,
(gdb) l
314     pub fn new_v1(pieces: &'a [&'a str],
315                   args: &'a [ArgumentV1<'a>]) -> Arguments<'a> {
316         Arguments {
317             pieces,
318             fmt: None,
319             args,
320         }
321     }
322
323     /// This function is used to specify nonstandard formatting parameters.

gormをしらべた(1)

  • https://gorm.io/docs/
  • gormを使っていたら中身が気になったので追ってみた
  • 公式のサンプルからとりあえず追う

dialectsMapはどこで初期化されているか

  • gorm.initからRegisterDialectが呼ばれている
  • ちなみに、このinitはpostgresなど各種にあり、それぞれ初期化が順に呼ばれる
func init() {
    RegisterDialect("common", &commonDialect{})
}
  • dialectsMapに渡されて初期化されている
// RegisterDialect register new dialect
func RegisterDialect(name string, dialect Dialect) {
    dialectsMap[name] = dialect
}

gorm.Open

  • 対象となるdbの種類とsourceを受け取り
  • sql.Openをしている
  • connectionが生きてるかどうかを判定するためにPingを送っている
func Open(dialect string, args ...interface{}) (db *DB, err error) {
    if len(args) == 0 {
        err = errors.New("invalid database source")
        return nil, err
    }
    var source string
    var dbSQL SQLCommon
    var ownDbSQL bool

    switch value := args[0].(type) {
    case string:
        var driver = dialect
        if len(args) == 1 {
            source = value
        } else if len(args) >= 2 {
            driver = value
            source = args[1].(string)
        }
        dbSQL, err = sql.Open(driver, source)
        ownDbSQL = true
    case SQLCommon:
        dbSQL = value
        ownDbSQL = false
    default:
        return nil, fmt.Errorf("invalid database source: %v is not a valid type", value)
    }

    db = &DB{
        db:        dbSQL,
        logger:    defaultLogger,
        callbacks: DefaultCallback,
        dialect:   newDialect(dialect, dbSQL),
    }
    db.parent = db
    if err != nil {
        return
    }
    // Send a ping to make sure the database connection is alive.
    if d, ok := dbSQL.(*sql.DB); ok {
        if err = d.Ping(); err != nil && ownDbSQL {
            d.Close()
        }
    }
    return
}

AutoMigrate

// Migrate the schema
db.AutoMigrate(&Product{})
  • AutoMigrateの中身がこれ
func (s *DB) AutoMigrate(values ...interface{}) *DB {
    db := s.Unscoped()
    for _, value := range values {
        db = db.NewScope(value).autoMigrate().db
    }
    return db
}
  • Unscopedというメソッドが実行されている
  • このメソッドは削除されたレコードを含む全てのレコードを返すようだ
// Unscoped return all record including deleted record, refer Soft Delete https://jinzhu.github.io/gorm/crud.html#soft-delete
func (s *DB) Unscoped() *DB {
    return s.clone().search.unscoped().db
}
  • 最初のcloneはこう
  • 新しいdbオブジェクトを生成している
  • 基本的に元のdbの情報を参照している大本は同じ物を参照しているので
  • 名前の通りcloneしている
  • newDialectはnewしているので別の物がいる
  • direct自体はsqlの方言を吸収するための物らしい
func (s *DB) clone() *DB {
    db := &DB{
        db:                s.db,
        parent:            s.parent,
        logger:            s.logger,
        logMode:           s.logMode,
        Value:             s.Value,
        Error:             s.Error,
        blockGlobalUpdate: s.blockGlobalUpdate,
        dialect:           newDialect(s.dialect.GetName(), s.db),
        nowFuncOverride:   s.nowFuncOverride,
    }

    s.values.Range(func(k, v interface{}) bool {
        db.values.Store(k, v)
        return true
    })

    if s.search == nil {
        db.search = &search{limit: -1, offset: -1}
    } else {
        db.search = s.search.clone()
    }

    db.search.db = db
    return db
}
  • s.dialect.GetName()はこの場合接続しているのがsqlite3なので
  • このようにsqlite3という文字列を返している
func (sqlite3) GetName() string {
    return "sqlite3"
}
  • newDialectはsqlite3とdbを引数に取る
  • dialectsMapにデータが入っているので
  • 特定dbのdirectへ接続情報をもたせて返す
  • valuesにはsync.Mapが入っているのでこのデータも新しいdbへ引き継がせる
func newDialect(name string, db SQLCommon) Dialect {
    if value, ok := dialectsMap[name]; ok {
        dialect := reflect.New(reflect.TypeOf(value).Elem()).Interface().(Dialect)
        dialect.SetDB(db)
        return dialect
    }

    fmt.Printf("`%v` is not officially supported, running under compatibility mode.\n", name)
    commontDialect := &commonDialect{}
    commontDialect.SetDB(db)
    return commontDialect
}
  • unscopedはsearchのUnscopedへtrueを立てて返す
func (s *search) unscoped() *search {
    s.Unscoped = true
    return s
}

First

  • Firstを呼んだ場合
  • 最初にNewCospeが呼ばれる
  • search.Limitを1に設定
func (s *DB) First(out interface{}, where ...interface{}) *DB {
    newScope := s.NewScope(out)
    newScope.Search.Limit(1)

    return newScope.Set("gorm:order_by_primary_key", "ASC").
        inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db
}

NewScope

  • 現在実行している命令のスコープを作成して返す
func (s *DB) NewScope(value interface{}) *Scope {
    dbClone := s.clone()
    dbClone.Value = value
    scope := &Scope{db: dbClone, Value: value}
    if s.search != nil {
        scope.Search = s.search.clone()
    } else {
        scope.Search = &search{}
    }
    return scope
}

callCallbacks

  • callCallbacksはs.parent.callbacks.queriesに設定されているクエリを順に実行する
    • queryCallback
    • preloadCallback
    • afterQueryCallback
func (scope *Scope) callCallbacks(funcs []*func(s *Scope)) *Scope {
    defer func() {
        if err := recover(); err != nil {
            if db, ok := scope.db.db.(sqlTx); ok {
                db.Rollback()
            }
            panic(err)
        }
    }()
    for _, f := range funcs {
        (*f)(scope)
        if scope.skipLeft {
            break
        }
    }
    return scope
}
  • callbacksはinitで登録されている
// Define callbacks for querying
func init() {
    DefaultCallback.Query().Register("gorm:query", queryCallback)
    DefaultCallback.Query().Register("gorm:preload", preloadCallback)
    DefaultCallback.Query().Register("gorm:after_query", afterQueryCallback)
}

queryCallback

  • ここまでの情報をもってqueryCallbackでクエリを組み立ててリクエストを送っている
// queryCallback used to query data from database
func queryCallback(scope *Scope) {
    if _, skip := scope.InstanceGet("gorm:skip_query_callback"); skip {
        return
    }

    //we are only preloading relations, dont touch base model
    if _, skip := scope.InstanceGet("gorm:only_preload"); skip {
        return
    }

    defer scope.trace(scope.db.nowFunc())

    var (
        isSlice, isPtr bool
        resultType     reflect.Type
        results        = scope.IndirectValue()
    )

    if orderBy, ok := scope.Get("gorm:order_by_primary_key"); ok {
        if primaryField := scope.PrimaryField(); primaryField != nil {
            scope.Search.Order(fmt.Sprintf("%v.%v %v", scope.QuotedTableName(), scope.Quote(primaryField.DBName), orderBy))
        }
    }

    if value, ok := scope.Get("gorm:query_destination"); ok {
        results = indirect(reflect.ValueOf(value))
    }

    if kind := results.Kind(); kind == reflect.Slice {
        isSlice = true
        resultType = results.Type().Elem()
        results.Set(reflect.MakeSlice(results.Type(), 0, 0))

        if resultType.Kind() == reflect.Ptr {
            isPtr = true
            resultType = resultType.Elem()
        }
    } else if kind != reflect.Struct {
        scope.Err(errors.New("unsupported destination, should be slice or struct"))
        return
    }

    scope.prepareQuerySQL()

    if !scope.HasError() {
        scope.db.RowsAffected = 0
        if str, ok := scope.Get("gorm:query_option"); ok {
            scope.SQL += addExtraSpaceIfExist(fmt.Sprint(str))
        }

        if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {
            defer rows.Close()

            columns, _ := rows.Columns()
            for rows.Next() {
                scope.db.RowsAffected++

                elem := results
                if isSlice {
                    elem = reflect.New(resultType).Elem()
                }

                scope.scan(rows, columns, scope.New(elem.Addr().Interface()).Fields())

                if isSlice {
                    if isPtr {
                        results.Set(reflect.Append(results, elem.Addr()))
                    } else {
                        results.Set(reflect.Append(results, elem))
                    }
                }
            }

            if err := rows.Err(); err != nil {
                scope.Err(err)
            } else if scope.db.RowsAffected == 0 && !isSlice {
                scope.Err(ErrRecordNotFound)
            }
        }
    }
}

気が向いたらもう少し追います

GO111MODULEの影響を追ってみた

main.go

func main() {
    _ = go11tag
    flag.Usage = base.Usage
    flag.Parse()
    log.SetFlags(0)

    args := flag.Args()
    if len(args) < 1 {
        base.Usage()
    }

    if args[0] == "get" || args[0] == "help" {
        if modload.Init(); !modload.Enabled() {
            // Replace module-aware get with GOPATH get if appropriate.
            *modget.CmdGet = *get.CmdGet
        }
    }

modload.Init()

  • src/cmd/go/internal/modload/init.go
  • ここでGO111MODULEをチェックしている
  • チェックして入ればMustUseModulesがtrueになる
func Init() {
    if initialized {
        return
    }
    initialized = true

    env := os.Getenv("GO111MODULE")
    switch env {
    default:
        base.Fatalf("go: unknown environment setting GO111MODULE=%s", env)
    case "", "auto":
        // leave MustUseModules alone
    case "on":
        MustUseModules = true
    case "off":
        if !MustUseModules {
            return
        }
    }
  • 更にその下の方に行くと
  • findModuleRootが空でmustUseModulesがfalseの場合
  • GOPATHを使った以前のモードだと解釈してreturnする
if CmdModInit {
    // Running 'go mod init': go.mod will be created in current directory.
    modRoot = cwd
} else {
    modRoot = findModuleRoot(cwd)
    if modRoot == "" {
        if !mustUseModules {
            // GO111MODULE is 'auto', and we can't find a module root.
            // Stay in GOPATH mode.
            return
        }
  • findModuleRootが何をしているかというと
  • go.modのあるディレクトリを探して返す
  • 無ければ空
func findModuleRoot(dir string) (root string) {
    dir = filepath.Clean(dir)

    // Look for enclosing go.mod.
    for {
        if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() {
            return dir
        }
        d := filepath.Dir(dir)
        if d == dir {
            break
        }
        dir = d
    }
    return ""
}
  • このmodRootはどこで使われているかというと
  • search.SetModRootにセットされています
  • 逆の言い方をすれば、go.modが無ければsearchにこのpathは含まれない
modfetch.GoSumFile = filepath.Join(modRoot, "go.sum")
search.SetModRoot(modRoot)
  • 結論を言うとGO111MODULEは
  • go.modを探す
  • modRootをsearch pathに含める
  • の2点を行わせているとみられる

goのmakeで起こりがちな悲劇

  • goのmakeを使っていると第3引数をあんまり使わないから
  • これ挙動なんだったっけとなるのでメモする
  • https://golang.org/pkg/builtin/

make(len:0)によるslice作成

  • よくあるやつ
  • 想像通りの結果
// cap: 0
v := make([]string, 0)

pp.Print(v)
fmt.Printf("len:%d,cap:%d\n", len(v), cap(v))

v = append(v, "hello")

pp.Print(v)
fmt.Printf("len:%d,cap:%d\n", len(v), cap(v))
  • 結果
[]string{}len:0,cap:0

[]string{
  "hello",
}len:1,cap:1

make(len:10)による起こりがちな悲劇

  • 気を利かせて先に領域を確保したはずなのに
  • 空の配列が出来てしまい
  • バグの要因になり
  • 更にappendすると無駄に領域を食べるという悲劇
// len:10
v := make([]string, 10)

pp.Print(v)
fmt.Printf("len:%d,cap:%d\n", len(v), cap(v))

v = append(v, "hello")
pp.Print(v)
fmt.Printf("len:%d,cap:%d\n", len(v), cap(v))

// cap: 10
v = make([]string, 0, 10)
  • 結果
[]string{
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
}len:10,cap:10
[]string{
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "",
  "hello",
}len:11,cap:20

makeで先に領域を確保しておきたい正しい書き方

  • 第3引数だけを指定
  • これにより、領域だけは確保されて
  • 空文字列の悲劇は起きない
  • appendしても無駄に増えない
// cap: 10
v := make([]string, 0, 10)

pp.Print(v)
fmt.Printf("len:%d,cap:%d\n", len(v), cap(v))

v = append(v, "hello")
pp.Print(v)
fmt.Printf("len:%d,cap:%d\n", len(v), cap(v))
  • 結果
[]string{}len:0,cap:10
[]string{
  "hello",
}len:1,cap:10

Contextとは何者なのか考えてみた(1)

  • goでですね、context.Contextがでてから割と立つんですが
  • 並行まわりを全部これでやらないといけ無いんじゃ無いかという強迫観念にとりつかれませんか
  • 素のchannnelやsync.WaitGroupとか使った方が早そうなんだけど
  • 違う道があるんじゃ無いかと考えてしまう
  • そこで、もう一度contextに対して色々いじって考えてみることにした

context.Contextとは何か?

  • 簡単に言うと以下のメソッドを要求するinterface
    • DeadLine
    • Done
    • Err
    • Value
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • 要求するメソッドを実装してさえ居ればContextと見なされます
  • これだけではなんなんで、言い換えてみると
  • timeoutとcancelを表現する為の共通の枠組みです

実際の実装: WithCancel

  • よく使われるContextに代入される実装は
  • contextに生えているメソッドから利用できます
  • その中でcancelを実装しているWithCancelが分かりやすいかと思うので
  • これを追います
  • 構造体はdoneチャンネルを保持しています
type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}
  • 細かい部分をはしょりますが
  • WithCancelが返すcancelの中身で以下の様にdonechannnelをcloseしています
if c.done == nil {
    c.done = closedchan
} else {
    close(c.done)
}
  • 更にこのdoneはDoneで参照されていて
  • closeされた場合はcloseされたチャンネルが返ります
  • つまり、渡したctxを介してchannelを使用してキャンセル処理を行うことができます
func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}
  • ここでよくあるcontextの定型処理をみます
  • channnelがcloseされていればcase文の中身が実行されて終了します
 select {
  case <-ctx.Done():
      fmt.Println("end")
      return
}

ここでふたたびcontextとは何か?

  • 現状channelを介してgoroutineへキャンセル処理を伝播させることの出来る共通の書式だと考えています。
  • この仕組みの導入によりDoneに相当する処理が彼方此方で書かれなくてもよくなり
  • contextを見たらそう言う処理があるんだろうなということが認識できるから分かりやすくなる
  • 本題はここからで、この枠組みをもって並列化させる仕組みは綺麗に書くことが出来るのか
  • sync.WaitGroupはどんな感じになっているかなどを次回以降に書く気になったら書きます

参考

雑な感じでappengineのcron.yamlをグラフィカルに表示したい

require 'yaml'

YAML.load_file('./cron.yaml')['cron'].each_with_index do |row,i|
    if m = row['schedule'].match(/^every\s+day\s+(.+)$/) then
        min_hour = m[1].split(/:/)
        puts "#{min_hour[1]} #{min_hour[0]} * * * #{i}:#{row['description']}"
    elsif m = row['schedule'].match(/^every\s+(\d+)\s+hours\s+(.+)$/) then
        puts "0 */#{m[1]} * * * #{i}:#{row['description']}"
    end
end
  • こんな感じにcron.yamlを適当にparseする
  • なんかいいparseクンはないものだろうか
  • そしてcronvで出力する
ruby convert-crontab.rb | ./cronv -o ./my_cron_schedule.html -d 24h

追記