haskell-ide-engineで ld: symbol(s) not found for architecture x86_64 のエラーが出る

vscode と haskell-ide-engine で Haskell 開発環境を構築する - Qiita をもとに環境構築を進めていたところ以下のエラーが出て、haskell-ide-engineをmakeすることができませんでした。

❯ make hie-8.6.3

(中略)

cabal-install-2.4.1.0: configure
Completed 19 action(s).

--  While building package cabal-install-2.4.1.0 using:
      /Users/cipepser/.stack/programs/x86_64-osx/ghc-8.6.3/bin/ghc --make -odir /private/var/folders/mc/3v_pttq16pdblh7vbqf4mvk80000gn/T/stack33371/cabal-install-2.4.1.0/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/setup -hidir /private/var/folders/mc/3v_pttq16pdblh7vbqf4mvk80000gn/T/stack33371/cabal-install-2.4.1.0/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/setup -i -i. -clear-package-db -global-package-db -package-db=/Users/cipepser/.stack/snapshots/x86_64-osx/nightly-2018-12-31/8.6.3/pkgdb -package-db=/Users/cipepser/hie/haskell-ide-engine/.stack-work/install/x86_64-osx/nightly-2018-12-31/8.6.3/pkgdb -hide-all-packages -package-id=Cabal-2.4.1.0-IaB5GUEm19R82R9cEdbB1D -package-id=base-4.12.0.0 -package-id=filepath-1.4.2.1 -package-id=process-1.6.3.0 -optP-include -optP/private/var/folders/mc/3v_pttq16pdblh7vbqf4mvk80000gn/T/stack33371/cabal-install-2.4.1.0/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/setup/setup_macros.h /private/var/folders/mc/3v_pttq16pdblh7vbqf4mvk80000gn/T/stack33371/cabal-install-2.4.1.0/Setup.hs /Users/cipepser/.stack/setup-exe-src/setup-shim-mPHDZzAJ.hs -main-is StackSetupShim.mainOverride -o /private/var/folders/mc/3v_pttq16pdblh7vbqf4mvk80000gn/T/stack33371/cabal-install-2.4.1.0/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/setup/setup -threaded
    Process exited with code: ExitFailure 1
    Logs have been written to: /Users/cipepser/hie/haskell-ide-engine/.stack-work/logs/cabal-install-2.4.1.0.log

    [1 of 2] Compiling Main             ( /private/var/folders/mc/3v_pttq16pdblh7vbqf4mvk80000gn/T/stack33371/cabal-install-2.4.1.0/Setup.hs, /private/var/folders/mc/3v_pttq16pdblh7vbqf4mvk80000gn/T/stack33371/cabal-install-2.4.1.0/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/setup/Main.o )
    [2 of 2] Compiling StackSetupShim   ( /Users/cipepser/.stack/setup-exe-src/setup-shim-mPHDZzAJ.hs, /private/var/folders/mc/3v_pttq16pdblh7vbqf4mvk80000gn/T/stack33371/cabal-install-2.4.1.0/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/setup/StackSetupShim.o )
    Linking /private/var/folders/mc/3v_pttq16pdblh7vbqf4mvk80000gn/T/stack33371/cabal-install-2.4.1.0/.stack-work/dist/x86_64-osx/Cabal-2.4.0.1/setup/setup ...
    clang-7: warning: argument unused during compilation: '-nopie' [-Wunused-command-line-argument]
    clang-7: warning: argument unused during compilation: '-nopie' [-Wunused-command-line-argument]
    ld: warning: ignoring file /Users/cipepser/.stack/snapshots/x86_64-osx/nightly-2018-12-31/8.6.3/lib/x86_64-osx-ghc-8.6.3/Cabal-2.4.1.0-IaB5GUEm19R82R9cEdbB1D/libHSCabal-2.4.1.0-IaB5GUEm19R82R9cEdbB1D.a, file was built for archive which is not the architecture being linked (x86_64): /Users/cipepser/.stack/snapshots/x86_64-osx/nightly-2018-12-31/8.6.3/lib/x86_64-osx-ghc-8.6.3/Cabal-2.4.1.0-IaB5GUEm19R82R9cEdbB1D/libHSCabal-2.4.1.0-IaB5GUEm19R82R9cEdbB1D.a
    Undefined symbols for architecture x86_64:
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziSetup_replDistPref_closure", referenced from:
          _s8sM_info in StackSetupShim.o
          _u8AR_srt in StackSetupShim.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziSetup_replVerbosity_closure", referenced from:
          _s8sZ_info in StackSetupShim.o
          _u8AT_srt in StackSetupShim.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziBuild_initialBuildSteps_closure", referenced from:
          _c8y6_info in StackSetupShim.o
          _u8AV_srt in StackSetupShim.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimple_defaultMainWithHooks_closure", referenced from:
          _s7o5_info in Main.o
          _s7o5_closure in Main.o
          _c8DE_info in StackSetupShim.o
          _u8EF_srt in StackSetupShim.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziUserHooks_replHook_closure", referenced from:
          _c8xN_info in StackSetupShim.o
          _c8y1_info in StackSetupShim.o
          _u8AW_srt in StackSetupShim.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimple_simpleUserHooks_closure", referenced from:
          _s7qo_info in Main.o
          _u7FE_srt in Main.o
          _c8xN_info in StackSetupShim.o
          _c8y1_info in StackSetupShim.o
          _s8u9_info in StackSetupShim.o
          _u8AW_srt in StackSetupShim.o
          _u8EE_srt in StackSetupShim.o
          ...
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziSetup_buildVerbosity_closure", referenced from:
          _s7p9_info in Main.o
          _u7Fs_srt in Main.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziUtils_installOrdinaryFiles_closure", referenced from:
          _r3TL_info in Main.o
          _r3TL_closure in Main.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziSetup_copyVerbosity_closure", referenced from:
          _s7pN_info in Main.o
          _u7Fv_srt in Main.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziSetup_copyDest_closure", referenced from:
          _s7q1_info in Main.o
          _u7Fx_srt in Main.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziLocalBuildInfo_absoluteInstallDirs_closure", referenced from:
          _s7nU_info in Main.o
          _u7wv_srt in Main.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziUtils_notice_closure", referenced from:
          _s7pm_info in Main.o
          _u7Fj_srt in Main.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziInstallDirs_NoCopyDest_closure", referenced from:
          _s7qn_info in Main.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziTypesziHookedBuildInfo_emptyHookedBuildInfo_closure", referenced from:
          _s8tV_info in StackSetupShim.o
          _u8EB_srt in StackSetupShim.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziUserHooks_UserHooks_con_info", referenced from:
          _c7xB_info in Main.o
          _c8DN_info in StackSetupShim.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziSetup_installVerbosity_closure", referenced from:
          _s7ql_info in Main.o
          _u7FA_srt in Main.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziInstallDirs_mandir_closure", referenced from:
          _s7nV_info in Main.o
          _u7ww_srt in Main.o
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziSimpleziFlag_fromFlag_closure", referenced from:
          _s7qk_info in Main.o
          _s7q0_info in Main.o
          _s7pM_info in Main.o
          _s7p8_info in Main.o
          _u7Fr_srt in Main.o
          _s8t0_info in StackSetupShim.o
          _s8sN_info in StackSetupShim.o
          ...
      "_Cabalzm2zi4zi1zi0zmIaB5GUEm19R82R9cEdbB1D_DistributionziTypesziLocalBuildInfo_buildDir_closure", referenced from:
          _s7nY_info in Main.o
          _s7pb_info in Main.o
          _s7pe_info in Main.o
          _u7wz_srt in Main.o
          _u7Fo_srt in Main.o
    ld: symbol(s) not found for architecture x86_64
    clang-7: error: linker command failed with exit code 1 (use -v to see invocation)
    `clang' failed in phase `Linker'. (Exit code: 1)
make: *** [cabal] Error 1

2019年1月末時点で、同じようなエラーでissueがいくつも立てられて(特にOSXで......)います。 ワークアラウンドも様々なようなので、今回は自分が解消した方法を残します。

環境、ビルド対象

OS

システムのバージョン:  macOS Mojave 10.14.3 (18D42)
カーネルのバージョン: Darwin 18.2.0

haskell-ide-engine

ビルド対象のcommit: d9bcbf28408da4c42e54cfd5014cfc1cce3ca993

なお、Makefile中で指定されているようにcabalのバージョンは2.4.1.0です。

brew

念のため。

❯ brew --version
Homebrew 2.0.0
Homebrew/homebrew-core (git revision 175af; last commit 2019-02-02)
Homebrew/homebrew-cask (git revision 05a81; last commit 2019-02-02)

解決策

Build failed on MacOS with clang link error(ghc 8.4.4) · Issue #1058 · haskell/haskell-ide-engineに書いてあるとおり、別プロジェクトでインストールしていたbinutilsが悪さをしていたようです。unlinkしたところ、うまくいきました。

※ issueではghc 8.4.4ですが、8.6.3でも同じ方法で解決しました。

❯ brew unlink binutils
Unlinking /usr/local/Cellar/binutils/2.31.1_2... 120 symlinks removed
❯ rm -rf ~/.stack
❯ stack clean --full

stack cleanrmの先にやってもいいかもしれないです。 この状態で、make hie-8.6.3したらエラーが解消され、以下のようにhieがインストールできました。

❯ hie --version
Version 0.5.0.0, Git revision d9bcbf28408da4c42e54cfd5014cfc1cce3ca993 (2394 commits) x86_64 ghc-8.6.3

なお、後続のmake build-doc-8.6.3も問題なくビルドできました。

その他

Makefileのどのコマンドで失敗していたか

cabal:
    stack install cabal-install
    cabal update
    cabal install Cabal-2.4.1.0 --with-compiler=$(GHC)
.PHONY: cabal

ここのstack install cabal-installで冒頭のエラーとなっていました。

他にも試してもうまくいかなかった方法たち

hie-install-memo.md

References

Writing A Compiler In Goを読んだ

Writing A Compiler In Goを読みました。

本書は、Writing An Interpreter In Go(邦訳:Go言語でつくるインタプリタ)の続編で、前作で開発したプログラミング言語MonkeyのコンパイラとVirtual Machine(以下、VM)を実装する内容となっています。

本書を読む前に、2018年後半にGo言語でつくるインタプリタを読み始めました。 Lexer、Parserを実装し、抽象構文木ASTを構築して、evalする流れを、自分の手でなぞり、ちゃんと動いたときには「Monkeyちゃん」と呼び出すほどに愛着が湧いていました。

これまで「すごそうだけどよくわからない」「いつか挑戦してみたい」と思っていた言語実装を一から自分の手でできたことに感動を覚え、 「Lexerは他にも使えそうだな」と、12月にはGo2 Advent Calendar 2018 - Qiitaにて以下のようにProtocol Buffersのデコーダを書いていました。

cipepser.hatenablog.com

そんな折、続編である本書Writing A Compiler In Goの存在を知りました。 Turing Complete FMがとても好きなpodcastなのですが、2018年のセキュリティ・キャンプのCコンパイラを自作してみよう!でも多数の方がコンパイラを実装していて、本当にみんなすごいなぁと憧れを抱いていました。 本書は、C言語ではないですが、コンパイラを実装できたことはとても嬉しかったです。

また、本書は現時点で邦訳が出ていません。個人的に初めて洋書を一冊完走することができた本にもなりました。 初めての洋書でしたが、思いの外すらすらと読み進めることができました。 理由としては以下がよかったです。

  • 一文一文が短いので、どこを読んでいるのか迷子にならない
  • シンプルな言い回しなので、文意がわかる
  • わからなくてもコードを見れば何を言っているのかわかる
  • やたら文章のテンションが高く、勢いがある
  • 「このテストは失敗して当然なんだ」のような応援が心強い

ちなみに、読むのにどれくらい時間が掛かるのか計測したところ26時間11分でした。 実装中にtypoでハマったりしたので参考ですが。

f:id:cipepser:20190120123326p:plain

また、インタプリタと合わせて書いたコードの量は9289行でした。 本書そのものがコメント(ドキュメント?)みたいなもので、コード中にはコメントがないので、処理そのものの行数だと考えると、気づけば結構書いたんだなぁと感慨深くもあります。

各章の所感

概要とメモベースで各章の所感を書きます。

第1章 Compilers & Virtual Machines

コンパイラVMとは何なのか、本書で実装するものが何なのかの説明。 コードがなく(例示のみあり)、ひたすら説明。

初めての洋書だったこともあり、本章が一番つらかったかもしれない。

第2章 Hello Bytecode!

バイトコードとは何か。どうやって表現し、実装するかの概要説明。

スタックを実装して、OpCodeをVMに解釈させて実行させるまでを行う。 JVMなど聞いたことはあったVMバイトコード、OpCodeがどういうものなのか、自分の手で実装することで掴めたのは大きかった。

第3章 Compiling Expressions

式をコンパイルする章。

まず中置式として四則演算のOpCodeから始まり、Booleanや比較演算子、前置式などを実装する。 同じことを繰り返しやっていくので、よくわからなくても進めていくうちに前のほうもわかるようになる。繰り返し大事。

True/False/Nullをグローバルオブジェクトとして実装すると比較が楽(こうしないとrefrect.DeeqEqualを使うことになる)のも、勉強になった。

第4章 Conditionals

条件式をバイトコードで表現する。

条件が真のときとそうでないときで、スタックの飛ぶ先をJUMP命令として実装する。これがあのJUMP命令か!と謎の感動があった。

第5章 Keeping Track of Names

Identifier、もとい名前がついた変数や関数をバイトコードでどうやって表現するか

変数名を数字で置き換えることは知っていたが、実際に実装したことあるかないかは違うと思う。 この章時点ではグローバル変数のみ実装。7章でScopeが実装される。

第6章 String, Array and Hash

章題の通り、StringArrayHashを実装する。

同じような内容の繰り返しなので、復習を兼ねてサクサク進む。IndexArrayHashで同一の実装にできるのが気持ちいい。

第7章 Functions

おそらく本書の山場。関数や関数呼び出しを実装する。

個人的にはこの章が一番おもしろかったけど、同時に一番頭を使った。テストを一行一行追いかけて、スタック上がどうなっているか考えると理解できると思う。一番エッジケースを考慮しないといけないものこの章だったと思う。

関数内でローカル変数を実装するため、Scopeを定義する。関数の引数もローカル変数と同じように実装できるのがよい。 VMのほうではScopeと同様の概念をFrameと呼んでいた。

第8章 Built-in Functions

前作で登場した組み込み関数を実装する。

7章のScopeを組み込み関数用に特別に用意する。 特に詰まることもなく進んだ。

第9章 Closures

クロージャを実装する。

7章でScopeの実装をしていたので、その延長ではあるものの自由変数の実装は興味深かかった。 グローバル変数ではなく自由変数を扱うために、前章までで関数をOpConstantでスタックに積んでいたが、 OpClosureを使って、関数と一緒に自由変数もスタックに積む。 これでうまく動くのかと懐疑的な気持ちだったが、テストが通る。すごい。

第10章 Taking Time

9章まででは関数を再帰で書けないので軽く修正を加える。

最後に、前作のevaluator v.s. vm (with compile)でベンチマークフィボナッチ数列の計算ベースで3倍程度速くなった。

最後に

思いは、本記事の序盤で語ってしまったので、多くは語りませんが、同じよう気持ちを持っている人には強く勧めます。 もちろん最適化や実装方法にも別の方式があるなど、本書の範囲外となっていることも多数あります。 (本書でも最適化よりわかりやすさ、学習を優先すると述べられている) しかし、入門書としてとても素晴らしい本だと思います。

なお、LexerやParserは、前作から引き続き利用するので、Go言語でつくるインタプリタから読むべきです。 前作から合わせるとクロージャや第一級オブジェクト、リテラルなどイマイチ掴めていなかった概念を、実装することで理解を深めることができました。

あとTDD(Table Driven Tests)を実践できるのも、Goのプラクティスを知る上で役立つと思います。 ただ、テストパターンはTypoで詰まると結構ハマる(1つのTypoで2時間くらい溶かしたりした)ので、コピペがおすすめです。(たぶんめんどくさくなるのもある)

本書は、Writing A Compiler In Go | Thorsten Ballで買えますが、eBookで買うとePub (iBook), Mobi (Kindle), PDF and HTMLで読めるのもよかったです。

References

Go言語でつくるインタプリタ

Go言語でつくるインタプリタ

  • 作者:Thorsten Ball
  • 発売日: 2018/06/16
  • メディア: 単行本(ソフトカバー)

165行で実装するProtocol Buffersデコーダ(ミニマム版)

この記事は Go2 Advent Calendar 2018の11日目の記事です。

今年の後半くらいに Protocol Buffers の仕様を読み始めたら、とてもシンプルかつコンパクトな仕様なのにcompatibilityへの考慮が凄まじくて、2018年後半に書いた記事の大半がProtocol Buffersに関するものでした。 仕様とバイナリを睨めっこしていたら、自分でもバイナリをデコードしたくなったので、実装してみました。

本内容は、あくまでProtocol Buffersの勉強を目的としたもので、仕様には完璧に添っていません。 というか、わかりやすさ(と実装のしやすさ)を優先して、コンパクトな仕様のさらにミニマム版な内容となっています。 当然ですが、実運用する際にはofficialの実装を利用してください。

どこまで実装するか

上述の通り、ミニマム版として、以下を実装範囲とします。

  1. バイナリからGoの構造体へUnmarshalする
  2. Unmarshalする構造体は既知とする
  3. wire typeは、値によって長さが変化する wire type 0(Varint)とwire type 2(Length-delimited)を対象とする

Length-delimitedVarintは後ほど説明するので、今は一旦飛ばしてOKです。

3.について、後述のLexerは、他のwire typeにも適用できるように設計してるので、長さが固定なことに注意すれば実装可能です。Future Workです。

ミニマム版ですが、Protocol Buffersでも最初にハマるVarintも含めているので、Protocol Buffersよくわからないという方にも、お伝えできることがあるんじゃないかと思います。 Varintの仕様がわかっていると値は127以下にするのが望ましい(バイナリが短くなる)とかも理解できるのうれしいです。

バイナリの生成

まずはofficalのエンコーダでバイナリを生成していきましょう。 正しく生成されたバイナリをデコードすることで、もとの構造体が復元できることを目標にします。

まず、すべての大元になる.protoファイルを以下のように定義します。
Person型にNameAgeのフィールドをもたせています。 wire type 2(Length-delimited)を表現したかったので、stringint32のままでよさそうなNameAgeをwrapしています。 (NameFirstNameLastNameとかにしたくなるかもしれないし......)

syntax = "proto3";

package person;

message Person {
  Name name = 1;
  Age age = 2;
}

message Name {
  string value = 1;
}

message Age {
  int32 value = 1;
}

protocして、上記.protoファイルに対応したライブラリ.pb.goをgenerateします。

❯ protoc -I=./ --go_out=./ person.proto

以下表の値を設定したバイナリを生成します。

フィールド
Name Alice
Age 20
package main

import (
    "io/ioutil"
    "log"

    pb "github.com/cipepser/protobufDecoder/Person"
    "github.com/golang/protobuf/proto"
)

func main() {
    p := &pb.Person{
        Name: &pb.Name{
            Value: "Alice",
        },
        Age: &pb.Age{
            Value: 20,
        },
    }

    if err := write("./person/alice.bin", p); err != nil {
        log.Fatal(err)
    }
}

func write(file string, p *pb.Person) error {
    out, err := proto.Marshal(p)
    if err != nil {
        return err
    }

    if err := ioutil.WriteFile(file, out, 0644); err != nil {
        return err
    }

    return nil
}

生成したperson/alice.binを適当なバイナリエディタで見てみると以下のようになります。 なお、vimなら:%!xxdで見れます。 hexdump person/alice.binをターミナル上で実行するのでもよいと思います。

0a07 0a05 416c 6963 6512 0208 14         ....Alice....

この時点で、バイナリではあるものの暗号化されているわけではない(Aliceが見えている)ので、ちょっと安心感を覚えます。

Protocol Buffersのバイナリの読み方

さて、実装を始める前にProtocol Buffersのバイナリがどのようなフォーマットなのか説明します。
Protocol Buffers のエンコーディング仕様の解説でも述べられているように以下が基本です。

key = タグナンバー * 8 + タイプ値

タグナンバーは.protoで定義した値です。 例えば、今回のAge age = 2;であれば、2がタグナンバーです。

タイプ値は、メッセージタイプを表す値で、 公式ドキュメントwire typesとして、以下のように定義されています。 冒頭からLength-delimitedVarintと言っていたやつです。

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

これだけだとよくわからないと思うので、例として、上で生成したバイナリをデコードしていきましょう。
改めて生成したバイナリを記載します。

0a 07 0a 05 41 6c 69 63 65 12 02 08 14

わかりやすいように色分けしました。

f:id:cipepser:20181208001319p:plain

一つずつ読んでいきます。

まず初めの0x0aは、
0d10 = タグname(1) * 8 + Length-delimited(type 2)
であることからタグナンバーとタイプ値がわかります。
Nameは自身で定義したmessageなので、表中のembedded messageが該当し、Length-delimitedとなります。
Length-delimited(type 2)だったので、lengthを得るために続く0x07を読みます。これがタグname(1)のlengthとなるので、後続の7バイト0a 05 41 6c 69 63 65Nameとしてデコードします。

Name(赤色の7バイト)の初め0x0aは、
0d10 = タグvalue(1) * 8 + Length-delimited(type 2)
です。
Length-delimited(type 2)だったので、lengthを得るために続く0x05を読みます。これがタグvalue(1)のlengthとなるので、後続の5バイト41 6c 69 63 65stringとして読んでいきます。 この5バイトをASCIIとして読むと41 6c 69 63 65Aliceとなります。

いい感じです。この調子で残りの12 02 08 14も読んでいきましょう。

0x12は、
0d18 = タグage(2) * 8 + Length-delimited(type 2)
です。
またまた、Length-delimited(type 2)だったので、lengthを得るために続く0x02を読みます。これがタグage(2)のlengthとなるので、後続の2バイト08 14Ageとしてデコードします。

0x08は、
0d08 = タグvalue(1) * 8 + Varint(type 0)
です。
やっとVarint(type 0)が登場しましたね。 Varint(type 0)はちょっとトリッキーなので、このあとすぐ説明します。 値が128未満であれば、そのままデコードしてあげることができるので、 今回の例では0x14int32として読んで0x14 = 0d20が得られます。

以上から、もともと定義したAlice20をデコードできました。

フィールド
Name Alice
Age 20

Varintの仕様について

Varint(type 0)について、もう少し詳しく見ていきます。 値が128未満であれば、そのままデコードできると述べたので、128以上の値として131としてみましょう。(128だと1が立つbitが一つしかないのでもう少しわかりやすく131にしました)

上記例で最後にVarintとして読み込んだ08 14(緑色の箇所)を思い出しましょう。

0x08 = 0d08 = タグvalue(1) * 8 + Varint(type 0)
からVarint(type 0)であることがわかり、 0x14int32としてデコードし、0d20を得ました。

.protoAge: 20,Age: 131,に変更してバイナリを生成し直してみます。 コードは上述と同じなので省略しますが、実行して得られるバイナリは、08 83 01となります。

先頭1バイトは変わらず0x08なので、value(tag 1),Varint(type 0)となるので83をVarintとして読み込みます。

ここで0x83を2進数で表記すると0b1000 0011です。
Varintでは、先頭1bitが1のとき、次の1バイトに数値が続いていることを表します。
次の1バイトを読み込むと0x01 = 0b0000 0001となり、先頭1bitが0となったため、読み込みはここで終了です。

あとは0x830x01を組み合わせてint32にデコードしてあげればいいのですが、仕様に以下のように書かれているので、リトルエンディアンで読んでいく必要があります。

varints store numbers with the least significant group first

また、先頭1bitは無視することにも注意です。

Varintの値
= 0x01(0b0000 0001)から先頭1bit落としたもの ++ 0x83(0b1000 0011)から先頭1bit落としたもの // リトルエンディアンなので0x01が先
= 000 0001 ++ 000 0011
= 0b1000 0011 // 先頭の6bitは0なので省略
= 0d131

以上より、131にデコードできます。

++演算子はバイナリを結合する操作を表します。

実装

バイナリの読み方がわかったところで実装に入っていきます。

Lexer

バイナリを読む部分を実装します。 ちょうど最近、Go言語でつくるインタプリタを読んでいるので、こちらの実装を大きく参考にさせて頂いています。

まず、Lexerを以下のように定義します。バイナリのバイト列bを1バイトずつ読んでいくため、 現在の位置positionと次のバイトの位置readPositionを保持します。

type Lexer struct {
    b            []byte
    position     int
    readPosition int
}

コンストラクタはバイト列inputを受け取って*Lexerを返します。

func New(input []byte) *Lexer {
    l := &Lexer{b: input}
    l.readPosition = l.position + 1
    return l
}

以下は補助的な役割を果たすメソッドですが、実装しておくと便利です。 バイト列がまだ読み込めるかをhasNext()で判断します。 また、next()で1バイト先に進められるようにします。

func (l *Lexer) hasNext() bool {
    return l.readPosition < len(l.b)
}

func (l *Lexer) next() {
    l.position++
    l.readPosition = l.position + 1
}

readCurByte()は現在の位置の1バイトを読み込み、位置を1つ進めます。 Varintを読み込む際はこちらを使います。

func (l *Lexer) readCurByte() byte {
    b := l.b[l.position]
    l.next()
    return b
}

readBytes(n int)readCurByte()のn文字版です。 Length-delimitedを読み込む際に利用します。
今回は省略しましたが、hasNext()EOFの判定を入れたほうがいいですね。

func (l *Lexer) readBytes(n int) []byte {
    bs := l.b[l.position : l.position+n]
    for i := 0; i < n; i++ {
        l.next()
    }
    return bs
}

Varintのデコード

Varintデコーダを実装します。
readCurByte()で1バイト読んできて、先頭1bitが1である限り(0になるまで)、1バイトずつ読み込みます。 先頭1bitは数値としてデコードする際には不要なので、0x7fとANDで論理積を取ります。 また、リトルエンディアンとしてデコードする必要があるので、スタックに積んでおきます。 スタックといいつつ、Goのcontainer/listはあんまり使われている印象がない(おそらくsliceのほうが速い?)ので、 bs[]byteappendすることにしました。
あとは先頭1bitを取り除いて、++で結合してあげればよいので、7bitほど左シフトさせて足しこんでいきます。

func (l *Lexer) decodeVarint() (uint64, error) {
    if len(l.b) == l.position {
        return 0, errors.New("unexpected EOF")
    }

    var bs []byte
    b := l.readCurByte()
    for bits.LeadingZeros8(b) == 0 { // 最上位bitが1のとき
        bs = append(bs, b&0x7f)
        b = l.readCurByte()
    }

    // 最上位bitが0のとき = 最後の1byte
    x := uint64(b)
    for i := 0; i < len(bs); i++ {
        x = x<<7 + uint64(bs[len(bs)-1-i])
    }

    return x, nil
}

Unmarshalの実装

PersonNameAgeそれぞれについて、Unmarshalの実装します。 どの型も流れは共通で、以下の流れになります。

  1. 1バイト読み込み、タグナンバーtagとタイプ値wireを計算する
  2. wireごとに後続の何バイト読み込むかがわかるので、その数だけ読み込む
  3. 読み込んだ値をtagに応じて評価する

1.のtagwireの計算は、

key = タグナンバー * 8 + タイプ値

であることを思い出すと、以下のように実装できます。

tag := key >> 3       // 下位4bit目以上を抜き出す
wire := int(key) & 7  // 下位3bitのみ抜き出す

tag0にしたとき

コラム的な話になりますが、tag0になるようなバイナリを与えると以下のようにpanicします。

panic: proto: person.Person: illegal tag 0 (wire type 2)

逆に0でないtagではpanic(どころかエラーにもならない)になりません。 今回の例でいうとPerson型はtag12しか定義していませんが、tagが3になるようなバイナリを読み込ませてもエラーにはなりません。 これはcompatibilityを考慮してのことで、tag3となるフィールドが増えた際に、 tag2までしかない古い.protoしか知らないクライアントでもエラーが起きないようにするためだと思われます。
今回の例(tag12しかない状態)で、tag3になるようにバイナリを作り、デコードすると、そのフィールドはnilになります。

panicさせる動作については、table_unmarshal.goに以下のように書かれています。

Explicitly disallow tag 0. This will ensure we flag an error when decoding a buffer of all zeros. Without this code, we would decode and skip an all-zero buffer of even length. [0 0] is [tag=0/wiretype=varint varint-encoded-0].

PersonのUnmarshal

今回の.protoで定義したPersonは以下のような構造体となります。

type Person struct {
    Name *Name // tag: 1
    Age  *Age  // tag: 2
}

このPerson型に対してUnmarshalを実装します。 今回Person型には、Length-delimited(type 2)になるフィールドしかないため、wireが意味を持つのはcase 2のときのみです。 tagName(1)Age(2)があるので、場合分けします。

func (p *Person) Unmarshal(b []byte) error {
    l := New(b)
    for l.hasNext() {
        key := uint64(l.readCurByte())
        tag := key >> 3
        wire := int(key) & 7

        switch wire {
        case 2:
            length := int(l.readCurByte())
            v := l.readBytes(length)

            switch tag {
            case 0:
                return errors.New("illegal tag 0")
            case 1:
                p.Name = &Name{}
                if err := p.Name.Unmarshal(v); err != nil {
                    return err
                }
            case 2:
                p.Age = &Age{}
                if err := p.Age.Unmarshal(v); err != nil {
                    return err
                }
            }
        default: // Person型はwire type 2以外は存在しない
            return fmt.Errorf("unexpected wire type: %d", wire)
        }
    }

    return nil
}

NameのUnmarshal

Personと同じように.protoで定義したNameは以下のような構造体となります。

type Name struct {
    Value string // tag: 1
}

このName型に対してUnmarshalを実装します。 今回Name型には、Length-delimited(type 2)になるフィールドしかないため、wireが意味を持つのはPersonと同じくcase 2のときのみです。 tagValue(1)のみなので、読み込んだバイト列をstringとしてデコードします。

func (n *Name) Unmarshal(b []byte) error {
    l := New(b)
    for l.hasNext() {
        key := uint64(l.readCurByte())
        tag := key >> 3
        wire := int(key) & 7

        switch wire {
        case 2:
            length := int(l.readCurByte())
            v := l.readBytes(length)

            switch tag {
            case 0:
                return errors.New("illegal tag 0")
            case 1:
                n.Value = string(v)
            }
        default: // Name型はwire type 2以外は存在しない
            return fmt.Errorf("unexpected wire type: %d", wire)
        }

    }
    return nil
}

AgeのUnmarshal

PersonNameと同じように.protoで定義したAgeは以下のような構造体となります。

type Age struct {
    Value int32 // tag: 1
}

このAge型に対してUnmarshalを実装します。 今回Age型には、Varint(type 0)になるフィールドしかないため、wireが意味を持つのはcase 0のときのみです。 tagValue(1)のみなので、読み込んだバイト列をdecodeVarint()int32としてデコードします。

func (a *Age) Unmarshal(b []byte) error {
    l := New(b)

    for l.hasNext() {
        key := uint64(l.readCurByte())
        tag := key >> 3
        wire := int(key) & 7

        switch wire {
        case 0:
            switch tag {
            case 0:
                return errors.New("illegal tag 0")
            case 1:
                i, err := l.decodeVarint()
                if err != nil {
                    return err
                }
                a.Value = int32(i)
            }
        default: // Age型はwire type 1以外は存在しない
            return fmt.Errorf("unexpected wire type: %d", wire)
        }

    }
    return nil
}

以上でデコーダ本体の実装は完了です。お疲れ様でした。

テスト

最後にテストを書いていきます。 今回の例で使ったバイナリ、ゼロ値(nil)になるパターン、マルチバイトになるVarintをテストパターンとします。

package decoder

import (
    "encoding/hex"
    "testing"

    "github.com/google/go-cmp/cmp"
)

func atob(s string) []byte {
    b, _ := hex.DecodeString(s)
    return b
}

func TestUnmarshalPerson(t *testing.T) {
    tests := []struct {
        b      []byte
        expect Person
    }{
        {
            // 今回の例
            b: atob("0a070a05416c69636512020814"),
            expect: Person{
                Name: &Name{Value: "Alice"},
                Age:  &Age{Value: 20},
            },
        },
        {
            // ゼロ値
            b:      atob(""),
            expect: Person{},
        },
        {
            // Ageのみゼロ値
            b: atob("0a070a05416c696365"),
            expect: Person{
                Name: &Name{Value: "Alice"},
            },
        },
        {
            // Nameのみゼロ値
            b: atob("12020814"),
            expect: Person{
                Age: &Age{Value: 20},
            },
        },
        {
            // Varintが2バイトになる場合
            b: atob("1203088301"),
            expect: Person{
                Age: &Age{Value: 131},
            },
        },
        {
            // Varintが3バイトになる場合
            b: atob("120408928002"),
            expect: Person{
                Age: &Age{Value: 32786},
            },
        },
    }

    for i, tt := range tests {
        p := Person{}
        if err := p.Unmarshal(tt.b); err != nil {
            t.Fatalf("test[%d - failed to Unmarshal. got err:%q", i, err)
        }
        if diff := cmp.Diff(p, tt.expect); diff != "" {
            t.Fatalf("test[%d - failed to Unmarshal. expected=%q, got=%q", i, tt.expect, p)
        }
    }
}

ちゃんとテストが通ります。

❯ go test ./decoder
ok      github.com/cipepser/protobufDecoder/decoder 0.006s

最後に

というわけで、Protocol Buffersのバイナリをデコードしてみました。 全体を表示しても以下のように165行程度なのでだいぶコンパクトに実装できたと思います。

f:id:cipepser:20181209140919p:plain

今回はPersonNameAgeとそれぞれUnmarshalを実装しましたが、同じような処理になるのでコード生成したいですね。 あと.protoからバイナリを生成するMarshalのほうとかも実装していきたいです。

ご指摘等あればtwitterまでご連絡ください。

References

Go言語でつくるインタプリタ

Go言語でつくるインタプリタ

Rustでマルチインターフェースパケットキャプチャを実装する

この記事は Rust Advent Calendar 2018 の3日目の記事です。

本記事では、タイトルの通り、Rustでマルチインターフェースパケットキャプチャを実装します。 今回の記事で達成したい目標は以下2点です。

  • Rustでネットワークプログラミングをしたい
  • マルチインターフェースにすることでマルチスレッド対応したい

どうやって低レイヤを扱うか

Rustでネットワークプログラミングを行うには、libpnetが便利なので、今回はこちらを利用します。libpnetを使えるようCargo.tomlには以下を記載しておきます。

[dependencies]
serde = "1.0"
serde_derive = "1.0"

[dependencies.pnet]
version = "0.21.0"

アーキテクチャ

今回実装するパケットキャプチャのアーキテクチャは以下です。

f:id:cipepser:20181202012641p:plain

I/F1からI/Fnの各インターフェース用のスレッドがrxを監視します。 rxで受信したパケットをqueueに渡します。 packet handlerは、queueにあるパケットを一つずつ処理(標準出力に表示)する役割を果たします。 tcpdumpなどでは対象や表示形式など引数を設定できますが、今回はL2-4までを対象として、表示対象を以下の通りとします。ARPは遊び心で加えました。

Layer 表示対象
L2 送信元MACアドレス
L2 宛先MACアドレス
L2-3 ARP request
L2-3 ARP reply
L3 送信元IPアドレス
L3 宛先IPアドレス
L4 送信元ポート番号
L4 宛先ポート番号

実装

パケットの実装

各パケットが どのインターフェースから着信したものか を保持するために、queueに渡すパケットを以下の構造体にまとめます。

#[derive(Clone, Debug)]
struct PacketWithInterface {
    interface: NetworkInterface,
    packet: Vec<u8>,
}

Queueの実装

各インターフェースのrxを監視する部分をマルチスレッドで実装します。 各スレッドからqueueを共有できるようArcMutexで包みます。

#[derive(Clone, Debug)]
struct Queue<T: Send> {
    inner: Arc<Mutex<VecDeque<T>>>,
}

上記Queueに対してnewgetaddを実装します。 注意が必要なのはgetaddを行う際にlockを取ってあげることくらいでしょうか。 なお、lockを取った際に_queueで束縛しており、_queueがスコープから外れたタイミングで自動的にunlockされます。

impl<T: Send> Queue<T> {
    fn new() -> Self {
        Self { inner: Arc::new(Mutex::new(VecDeque::new())) }
    }

    fn get(&self) -> Option<T> {
        let _queue = self.inner.lock();
        if let Ok(mut queue) = _queue {
            queue.pop_front()
        } else {
            None
        }
    }

    fn add(&self, obj: T) -> usize {
        let _queue = self.inner.lock();
        if let Ok(mut queue) = _queue {
            queue.push_back(obj);
            queue.len()
        } else {
            0
        }
    }
}

packet handlerの実装

queueからパケットを取り出す部分はmain内で実装するので、ここでは取り出したパケットの処理することに注力します。 基本的にはバイト列の先頭からL2 -> L3 -> L4と、各レイヤーのヘッダを確認し、該当するフィールドを表示する処理になります。 なお、ARPパケットはEthernetヘッダに記載されているEtherTypeで判断できます。

fn handle_ethernet_frame(interface: &NetworkInterface, ethernet: &EthernetPacket) {
    println!(
        "{}: {} > {}",
        interface.name,
        ethernet.get_source(),
        ethernet.get_destination(),
    );

    print!("  {}: ", ethernet.get_ethertype());
    match ethernet.get_ethertype() {
        EtherTypes::Arp => {
            let arp = arp::ArpPacket::new(ethernet.payload()).unwrap();
            match arp.get_operation() {
                arp::ArpOperations::Reply => {
                    println!(
                        "ARP reply({}): {} -> {}",
                        arp.get_sender_proto_addr(),
                        arp.get_sender_hw_addr(),
                        arp.get_target_hw_addr()
                    );
                }
                arp::ArpOperations::Request => {
                    println!(
                        "ARP request({}): {} -> {}",
                        arp.get_target_proto_addr(),
                        arp.get_sender_hw_addr(),
                        arp.get_target_hw_addr()
                    );
                }
                _ => (),
            }
        }
        EtherTypes::Ipv4 => {
            let ip = Ipv4Packet::new(ethernet.payload()).unwrap();
            println!("{} -> {}", ip.get_source(), ip.get_destination());
            handle_ip_packet(&interface, &ip)
        }
        _ => (),
    }
}

fn handle_ip_packet(interface: &NetworkInterface, ip: &Ipv4Packet) {
    print!("    {}: ", ip.get_next_level_protocol());
    match ip.get_next_level_protocol() {
        IpNextHeaderProtocols::Tcp => {
            let tcp = tcp::TcpPacket::new(ip.payload()).unwrap();
            handle_tcp_packet(&interface, &tcp);
        }
        IpNextHeaderProtocols::Udp => {
            let udp = udp::UdpPacket::new(ip.payload()).unwrap();
            handle_udp_packet(&interface, &udp);
        }
        _ => (),
    }
}

fn handle_tcp_packet(_interface: &NetworkInterface, tcp: &tcp::TcpPacket) {
    println!("{} -> {}", tcp.get_source(), tcp.get_destination());
}

fn handle_udp_packet(_interface: &NetworkInterface, udp: &udp::UdpPacket) {
    println!("{} -> {}", udp.get_source(), udp.get_destination());
}

mainの実装

インターフェースを初期化

キャプチャ対象とするインターフェースをここで指定します。 configファイルから読み取る実装もしてみましたが、今回は簡単のためmainにべた書きとしました。

let interface_names: HashSet<&str> = vec!["RT2_veth0", "RT2_veth1"]
    .into_iter()
    .collect();

let interfaces: Vec<NetworkInterface> = datalink::interfaces()
    .into_iter()
    .filter(|interface: &NetworkInterface| interface_names.contains(interface.name.as_str()))
    .collect();

パケットを受信する

各インターフェースのrxを取得して、ループでrxを監視します。そしてrxで受信したパケットをqueueに投げ込みます。
今回の実装でqueueに投げ込む部分が一番ハマりました。 rx.next()で受信できるパケットは&[u8]なので参照を渡そうとすると、spawnで作ったクロージャより長生きできず全然コンパイルが通りませんでした。

let mut handles: Vec<_> = interfaces.into_iter()
    .map(|interface| {
        let queue = queue.clone();
        thread::spawn(move || {
            let mut rx = datalink::channel(&interface, Default::default())
                .map(|chan| match chan {
                    Ethernet(_, rx) => rx,
                    _ => panic!("could not initialize datalink channel {:?}", interface.name),
                }).unwrap();

            loop {
                match rx.next() {
                    Ok(src) => {
                        queue.add(PacketWithInterface {
                            interface: interface.clone(),
                            packet: src.to_owned(),
                        });
                    }
                    Err(_) => {
                        continue;
                    }
                }
            }
        })
    }
    )
    .collect();

パケットを処理する

queueからパケットを取り出し、EthernetPacketmatchした後、handle_ethernet_frameに渡します。渡した後は上述のpacket handlerの責務です。 異常パケットを受信した場合はパケットをドロップし、無視します。異常パケット1つ受信しただけでpanicしてプロセスごと落ちない設計としました。

handles.push(thread::spawn(move || {
    loop {
        let queue = queue.clone();
        match queue.get() {
            Some(packet_with_interface) => {
                let _packet = packet_with_interface.packet.deref();
                match EthernetPacket::new(_packet) {
                    Some(packet) => {
                        handle_ethernet_frame(&packet_with_interface.interface, &packet);
                    },
                    _ => {
                        continue;
                    }
                }
            },
            _ => {
                continue;
            }
        }
    }
}));

実際に動かしてみる

環境

検証用の環境は以下の通りです。

$ uname -a
Linux vagrant-ubuntu-trusty-64 3.13.0-161-generic #211-Ubuntu SMP Wed Oct 3 14:52:35 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

ネットワーク構築

netnsで以下のネットワークを用意します。 構築スクリプトこちら

RT2の★マークの箇所RT2_veth0RT2_veth1がパケットキャプチャの対象インターフェースです。

f:id:cipepser:20181202011833p:plain

pingを打ったときのパケットキャプチャ

host192.168.0.1からRT1192.168.1.254pingを打ってみます。

# host
$ ping 192.168.1.254 -c 1
PING 192.168.1.254 (192.168.1.254) 56(84) bytes of data.
64 bytes from 192.168.1.254: icmp_seq=1 ttl=63 time=0.706 ms

--- 192.168.1.254 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.706/0.706/0.706/0.000 ms

このときのパケットキャプチャは以下のようになりました。

# RT2
$ sudo ./target/debug/pcap
RT2_veth1: d6:c7:d0:99:8e:bc > ff:ff:ff:ff:ff:ff
  Arp: ARP request(192.168.1.254): d6:c7:d0:99:8e:bc -> 00:00:00:00:00:00
RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc
  Arp: ARP reply(192.168.1.254): f6:63:73:99:cc:cd -> d6:c7:d0:99:8e:bc
RT2_veth1: d6:c7:d0:99:8e:bc > f6:63:73:99:cc:cd
  Ipv4: 192.168.0.1 -> 192.168.1.254
    Icmp: RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc
  Ipv4: 192.168.1.254 -> 192.168.0.1
    Icmp: RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc

最初はまったく通信がないので、ARPによってIPアドレスからMACアドレスの解決を行っています。 その後、192.168.0.1から192.168.1.254へのICMP通信が発生し、その応答が返ってきていることがわかります。

TCP通信を発生させたときのパケットキャプチャ

RT1で以下のように1234ポートでリッスンしておきます。

# RT1
$ nc -l -p 1234

host192.168.0.1からRT1192.168.1.254:1234へ接続します。

# host
$ nc 192.168.1.254 1234

このときのパケットキャプチャは以下のようになりました。

# RT2
RT2_veth1: d6:c7:d0:99:8e:bc > f6:63:73:99:cc:cd
  Ipv4: 192.168.0.1 -> 192.168.1.254
    Tcp: 45693 -> 1234
RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc
  Ipv4: 192.168.1.254 -> 192.168.0.1
    Tcp: 1234 -> 45693
RT2_veth1: d6:c7:d0:99:8e:bc > f6:63:73:99:cc:cd
  Ipv4: 192.168.0.1 -> 192.168.1.254
    Tcp: 45693 -> 1234

正しくキャプチャできていますね。 45693は送信元なのでhighポートがOSによって割り当てられています。 また、3つ目のパケットから、ncコマンドでリッスンしていると応答がないのでTCPの再送が行われていることがわかります。

最後に

以上、Rustによるマルチインターフェースパケットキャプチャでした。 本文中でも述べましたがlifetimeですごいハマりました。 少しずつRustを勉強していますが、まだまだですね。今回みたいになにか作っているといろいろハマって理解が深まるので前向きにがんばります。

ご指摘等あればtwitterまでご連絡ください。

References

【GAS】「SpreadsheetApp.create を呼び出す権限がありません」を解決する

表題の通りです。
タイトルのエラーメッセージでググったところ、トリガーを設定し、解決されている方が多いようです。 しかし、当方の要件としては、トリガーで定期的に実行させる必要はなく、エラーメッセージは権限の問題なのでスコープを設定したかったので、appsscript.jsonでスコープを設定する方法を残します。

余談ですが、GUIから設定できるのかと思ったので、探してみたのですが、 ファイル -> プロジェクトのプロパティ -> スコープタブでは、権限が確認できるのみで変更はできなかったので、appsscript.jsonに以下を追加しました1

{
  "oauthScopes": ["https://www.googleapis.com/auth/spreadsheets.readonly"],
}

なお、参照権限のみで十分であれば、https://www.googleapis.com/auth/spreadsheets.readonlyのようにreadonlyを付けましょう。

References


  1. 必要な箇所のみ記載して、前後は省略しています。

clasp runがローカルで実行されない

背景

Google Apps Scriptをローカルで開発するために、 Google製のCLIツールclaspを導入しました。
Google Drive上のプロジェクトとclasp pushclasp pullで同期できてとても便利なのですが、 以下事象でハマったので記事に残します。

事象

  1. ローカルでコードを編集
  2. clasp pushする
  3. clasp run <function name>すると、2.でpushしたコードが実行されない

切り分けをしたところ

  • ブラウザ上のプロジェクトで見ると公開 - 実行可能APIとして導入...が2.で上げたバージョンになっていない

が原因のようでした。

解決方法

--devオプションをつければローカルで実行されます。

❯ clasp run --dev <function name>

References

protobufのNon-varintの互換性について

以前、Varintを64bitから32bitに変更したときの挙動を検証しました。

cipepser.hatenablog.com

上記記事の通りVarintは、下位32-bitを残して切り詰める結果でした。 今回は、Non-varintの互換性を検証します。

例によって、Language Guideを見てみると

fixed32 is compatible with sfixed32, and fixed64 with sfixed64.

と述べられています。

fixed32sfixed32fixed64sfixed64のように同じ長さであれば、互換性があるようです。

実際、Protocol Buffers - Encodingwire typesで以下のように定義されており、32bitと64bitでそもそもTypeが異なります。

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

.proto

今回は以下の.protoを使います。

syntax = "proto3";
package fixed;

message User {
  fixed32 id = 1;
}

idfixed32fixed64sfixed32sfixed64の4パターンで変化させて、検証します。

書き出し

書き出し用のコードは以下の通りです。 Idvalueはbitがすべて1になる値で検証します(user.Idフィールドのコメントアウト参照)。

package main

import (
    "io/ioutil"
    "log"

    pb "github.com/cipepser/protobuf-sample/fixed"
    "github.com/golang/protobuf/proto"
    "fmt"
    // "math" // fixed32とfixed64のときのみ利用
)

func main() {
    user := &pb.User{
        //Id: math.MaxUint32, // fixed32のとき
        //Id: math.MaxUint64, // fixed64のとき
        Id: -1,  // sfixed32とsfixed64のとき
    }

    fmt.Printf("%x\n", user.Id)

    if err := write("./fixed/fixed32.bin", user); err != nil {
        log.Fatal(err)
    }
}

func write(file string, user *pb.User) error {
    out, err := proto.Marshal(user)
    if err != nil {
        return err
    }
    fmt.Printf("% x\n", out)
    if err := ioutil.WriteFile(file, out, 0644); err != nil {
        return err
    }
    return nil
}

出力ファイルfixed32.binの部分は、fixed32fixed64sfixed32sfixed64ごとに別ファイルとして出力します。

読み込み

読み込み用のコードです。

package main

import (
    "fmt"
    "io/ioutil"
    "log"

    pb "github.com/cipepser/protobuf-sample/fixed"
    "github.com/golang/protobuf/proto"
)

func main() {
    if err := read("./fixed/fixed32.bin"); err != nil {
        log.Fatal(err)
    }
    if err := read("./fixed/fixed64.bin"); err != nil {
        log.Fatal(err)
    }
    if err := read("./fixed/sfixed32.bin"); err != nil {
        log.Fatal(err)
    }
    if err := read("./fixed/sfixed64.bin"); err != nil {
        log.Fatal(err)
    }
}

func read(file string) error {
    in, err := ioutil.ReadFile(file)
    if err != nil {
        return err
    }
    user := &pb.User{}

    if err := proto.Unmarshal(in, user); err != nil {
        return err
    }
    fmt.Printf("%d\n", user.Id)
    return nil

}

読み込みの前に、.protoidfixed32fixed64sfixed32sfixed64で変化させて、protoc -I=fixed/ --go_out=fixed/ fixed.protoを実行しています。

結果

書き出し、読み込みを全パターンで行った結果は以下の通りです。
※表のレイアウトが崩れるため、4294967295(math.MaxUint32)18446744073709551615(math.MaxUint64)MaxUint32MaxUint64で略記します。

正しく読み込めている()のは、書き出し/読み込みで同じ.protoを使ったパターンのみでした。

.proto(write時) 値(write) .proto(read時) 値(read) 結果
fixed32 MaxUint32 fixed32 MaxUint32
fixed32 MaxUint32 fixed64 0 ×
fixed32 MaxUint32 sfixed32 -1
fixed32 MaxUint32 sfixed64 0 ×
fixed64 MaxUint64 fixed32 0 ×
fixed64 MaxUint64 fixed64 MaxUint64
fixed64 MaxUint64 sfixed32 0 ×
fixed64 MaxUint64 sfixed64 -1
sfixed32 -1 fixed32 MaxUint32
sfixed32 -1 fixed64 0 ×
sfixed32 -1 sfixed32 -1
sfixed32 -1 sfixed64 0 ×
sfixed64 -1 fixed32 0 ×
sfixed64 -1 fixed64 MaxUint64
sfixed64 -1 sfixed32 0 ×
sfixed64 -1 sfixed64 -1

上記結果を見ると、 パースそのものに失敗して、値が0となった(×)ものと、 値は読み込めているものの元の値と異なる()もの があることがわかります。

よくよく見てみると×は32bitと64bitで長さが異なるもののみです。

Language Guide

fixed32 is compatible with sfixed32, and fixed64 with sfixed64.

の通りですね。
しかし、同じ長さであればcompatibleなはずなのにで元の値と変わってしまったものもあります。
もう少し深掘りするために、書き出したバイナリを表に加えてみます。(binary列)

proto(write時) 値(write) binary proto(read時) 値(read) 結果
fixed32 MaxUint32 0d ff ff ff ff fixed32 MaxUint32
fixed32 MaxUint32 0d ff ff ff ff fixed64 0 ×(32bit vs 64bit)
fixed32 MaxUint32 0d ff ff ff ff sfixed32 -1
fixed32 MaxUint32 0d ff ff ff ff sfixed64 0 ×(32bit vs 64bit)
fixed64 math.MaxUint64 09 ff ff ff ff ff ff ff ff fixed32 0 ×(32bit vs 64bit)
fixed64 math.MaxUint64 09 ff ff ff ff ff ff ff ff fixed64 math.MaxUint64
fixed64 math.MaxUint64 09 ff ff ff ff ff ff ff ff sfixed32 0 ×(32bit vs 64bit)
fixed64 math.MaxUint64 09 ff ff ff ff ff ff ff ff sfixed64 -1
sfixed32 -1 0d ff ff ff ff fixed32 MaxUint32
sfixed32 -1 0d ff ff ff ff fixed64 0 ×(32bit vs 64bit)
sfixed32 -1 0d ff ff ff ff sfixed32 -1
sfixed32 -1 0d ff ff ff ff sfixed64 0 ×(32bit vs 64bit)
sfixed64 -1 09 ff ff ff ff ff ff ff ff fixed32 0 ×(32bit vs 64bit)
sfixed64 -1 09 ff ff ff ff ff ff ff ff fixed64 math.MaxUint64
sfixed64 -1 09 ff ff ff ff ff ff ff ff sfixed32 0 ×(32bit vs 64bit)
sfixed64 -1 09 ff ff ff ff ff ff ff ff sfixed64 -1

まず、追加したbinary列の先頭の0d09でkeyのfield_numberwire_typeがわかります。
Protocol Buffers - Encodingに書いてあるとおり、keyは、(field_number << 3) | wire_typeエンコードされます。

つまり、
0d(1 << 3) | 5なので、type5(32-bit)のフィールド1(Id)であり、
09(1 << 3) | 1なので、type5(64-bit)のフィールド1(Id)です。

その後に続くff...value1です。

まとめ

以上から、×となった32bitと64bitは0d09でそもそものwire_typeが異なり、パースに失敗し、defalut valueである0となったようです。

となった箇所については、wire_typeが同じ(fixed32sfixed320dfixed64sfixed6409)ため、後続のバイナリをvalueとしてパースします。 パースする際に.protoが異なるため、書き込んだ値と違う値で解釈されてしまったわけです。 例えば、fixed32sfixed32では、 fixed32としてMaxUint32(4294967295)0xff ff ff ffとしてエンコードします。 しかし、sfixed320xff ff ff ffをパースすると-1として解釈されてしまいます。

パースに際してエラーが起きるわけではない2ので、正しく解釈できる範囲でcompatibleです。 上記の例(32bit)では、0x00 00 00 000x07 ff ff ffです。 これを超えると、正数と2の補数で表現された負数が区別できないため、signed/unsignedで結果が異なります。

References


  1. 今回はffとなっているため区別しませんが、valueはホストバイトオーダーで書き込まれるようなので、リトルエンディアンになることが多いと思います。バイナリを読む際にはご注意ください。

  2. 処理系によって異なるかもしれません。今回検証したGoの範囲での結果です。