Cコンパイラを作り始めた

タイトルの通り、コンパイラを作り始めました。
先日、 転職のお知らせ にも書いたのですが、低レイヤーを勉強していきたいとここ最近考えていました。
正直、僕が考えていた低レイヤーというのは、AWS上でインフラを構築できるようになることやRDBのチューニングでした(前職ではアプリケーションのコードを書くこと以外、あまり気にしなくてよかったのでこのへんをやりたかった)。
しかし、会社の同僚の方たちと飲んでいるときに、エモい低レイヤーの話を聞き、ちょっとやってみるか!という気持ちになったのでコンパイラを作り始めてみました。

作っているもの

9ccというCコンパイラを作っています。
Cを書いた経験はなく、けっこう苦しいところもあります...

github.com

Cコンパイラとは、C言語で書かれたプログラムをアセンブリ言語に変換するものです。
例えば 5+20-4 の場合、以下のようなアセンブリを出力します(僕の現時点のコードでは)

.intel_syntax noprefix
.global main
main:
  push 5
  push 20
  pop rdi
  pop rax
  add rax, rdi
  push rax
  push 4
  pop rdi
  pop rax
  sub rax, rdi
  push rax
  pop rax
  ret

9ccという名前を聞いて、ピンと来る方もいるかもしれませんが、8cc9cc 作者の Rui Ueyama さんが書かれている 低レイヤを知りたい人のためのCコンパイラ作成入門 を参考にしながら(現時点ではほぼ写経しながら)作っています。

実装が完了しているもの

  • 足し算、引き算
  • 空白を含む足し算、引き算
  • 加減乗算、優先順位のカッコを含む計算
  • 単項プラス、単項マイナス

と、書いてもよくわからないと思うので、テストコードを見てもらうのがわかりやすいです。

tryという関数に期待値と入力値を渡し、実際の結果が期待値と一致するかをチェックしています。
try {期待値} {入力値}

# 整数1個だけコンパイルできる
try 0 0
try 42 42

# 足し算、引き算対応
try 21 '5+20-4'

# 空白対応
try 41 ' 12 + 34 - 5 '

# 加減乗算と優先順位のカッコからなる式に対応
try 47 '5+6*7'
try 15 '5*(9-6)'
try 4 '(3+5)/2'

# 単項プラスと単項マイナス
try 5 '-10+15'
try 15 '14+-(-7)-(+6)'

地道にやっていく感じですね。 できることが増えてくると面白いです。

実装中の箇所

  • 比較演算子 (== != <= >=)
    • ちょっとハマっている...

これから実装すること

  • ファイルを分けて分割コンパイルできるようにする
  • 変数、関数に対応

基本、 低レイヤを知りたい人のためのCコンパイラ作成入門 に沿って実装していく予定です。
挫折しなければ😂

最後に

APIなどのアプリケーションのコード書くのとは全然違いますが、ちょっと楽しいので地道に続けていきたいです。
そして、いつかGoでGoのコンパイラ作ってみたいです

参考文献

www.sigbus.info

nor-isio.hateblo.jp

転職のお知らせ

2019年4月12日でピックアップを退職します。
3月19日が最終出社日で、在籍期間は1年9ヶ月ほどでした。
3年くらい働いたような感じがしますが、実際は2年も働いていませんでした。

誰?

inari111 というIDで活動しています。
オークファン→BASE→ピックアップで働いてきました。
主にサーバーサイドの開発をしてきました。以前はPHPを書いていましたが、今はGoが好きです。
退職エントリ、入社エントリを読むのが好きです。今回は書く側になりました。

何をやってきたか

  • Go
  • Protobuf on HTTP
  • GCP
    • App Engine (以下GAE)
    • Datastore
    • Cloud Build
    • その他いろいろ
  • Firebase
    • Auth
    • Cloud Functions
    • その他いろいろ
  • CircleCI

APIや管理画面はGAEで動かしていたのですが、こんな便利なものがあるのかと感じるくらいGAEは便利でした。
インフラ面の心配をすることなく、アプリケーションのコードを書くことに集中できるので、人数の少ないチームには合っていると思います。
Goを書ける環境は楽しかったですし、レイヤードアーキテクチャやProtobufを使った開発を経験できたのもよかったと感じています。

最初の半年くらいはサーバーサイドエンジニアが足りていない状況で、機能追加時にはだいぶ迷惑をかけてしまいました。
完全に僕のスキル不足、経験不足によるものだと思っています。
1サービスに対しサーバーサイドエンジニアが2人を超えたあたりから、徐々に開発に余裕が生まれるようになったと思います。
入社してくれた方々が新しい風を吹かせてくれるのはとても心地よく、技術的なことだけでなく仕事の進め方や他職種とのコミュニケーションの取り方は勉強になりました。
一緒に働けたことに感謝しています。

なんで辞めるのか

数年後を見据えると、今のスキルセットでエンジニアを続けていくことに不安がありました。
GAEのような抽象度の高いサービスを利用した開発ではなく、今のうちに低レイヤーに触れておくべきだろうというのはここ数ヶ月感じていたことです。
年齢が上がるにつれて未経験の領域に挑戦するのはハードルが上がると思っていて、挑戦しやすい今のうちに思い切って転職することにしました。

どのように転職活動をしたか

下記のサービス経由で話を聞きに行ったり、選考を受けたりしました。

今までの転職活動では2~3社程度しか受けていなかったので、今回は多く受けようと考えていました。
20人くらいの規模の会社からメガベンチャーまで、幅広く受けています。
数人規模のスタートアップは、今の自分とは合わないだろうなと考えていたので、受けていません。
働きながら転職活動するのはしんどいので、もうしたくないですね...

転職活動で重視したこと

  • Goが書けること
  • エンジニアチームの雰囲気、考え方が自分に合っているかどうか
  • 新しいことに挑戦できる
    • インフラ、Webフロントエンド等
  • RDB触れること
  • エンジニアとして働きやすい企業かどうか
  • 経験豊富なエンジニアがいること

次何するの?

  • Goでアプリケーションのコードを書く
  • インフラもやる
  • Webフロントエンドもやる(Goを書くのがメインになる予定)

内定をいただいた企業の中から一番自分に合いそうな企業を選びました。

次働く企業は、入社後にまた改めて書きたいと思います。
聞かれたら答えているのでDMでも送ってください。

最後に

人数が少ないときに入社することができ、カオスな時期から人が増えていくフェーズを経験できたのは自分にとって貴重な経験でした。
日々新しいことを学ぶことができる環境は非常に楽しかったですし、周りの優秀な方から受ける影響も大きかったです。
今まで働いてきた中で一番働きやすい環境だったと思います。

子供の頃から、新しい環境に対してワクワクした気持ちよりも不安な気持ちを抱くことが多く、居心地のいい環境を出てまでやりたいことを優先している自分に少しびっくりしていますが、今後もコツコツとやっていけたらと思います。

では! 

 

 

【メモ】 DockerでMySQLサーバーコンテナを起動する

バージョンを指定してイメージの取得

docker pull mysql:5.7.25

MySQLサーバーコンテナを起動し、DATABASEを作成したりする

  • hoge というDBの作成
  • inari111 というユーザーの作成
  • -eオプションは環境変数を設定
  • -d detachといい、バックグラウンドでコンテナを起動
docker run --name {コンテナ名} -e MYSQL_DATABASE=hoge -e MYSQL_USER=inari111 -e MYSQL_PASSWORD=hogehoge  -e MYSQL_ROOT_PASSWORD={rootユーザーパスワード} -d mysql

コンテナに接続

docker run --link {コンテナ名}:mysql -it --rm mysql bash

-i : ホストの入力をコンテナの標準出力をつなげる
-t : コンテナの標準出力とホストの出力をつなげる

(シェルのような)インタラクティブなプロセスでは、コンテナのプロセスに対して tty を割り当てるために、 -i -t を一緒に使う必要があります。 後の例で出てきますが -i -t は -it と書けます。

Docker run リファレンス

クリーンアップ (--rm)

デフォルトではコンテナを終了しても、コンテナのファイルシステム(の内容)を保持し続けます。これにより、多くのデバッグをより簡単にします(最後の状態を確認できるため)。そして、全てのデータを維持し続けるのがデフォルトです。しかし、短い期間だけ フォアグラウンド で動かしたとしても、これらのコンテナのファイルシステムが溜まり続けます。そうではなく、 コンテナの終了時に、自動的にコンテナをクリーンアップし、ファイルシステムを削除する には --rm フラグを追加します。

コンテナ内でのコマンドの実行が終わったらコンテナを自動で削除するということ。

MySQLに接続

hostはenvコマンドを叩いて表示された MYSQL_PORT_3306_TCP_ADDR を使う

mysql -u inari111 -p hogehoge -h {MYSQL_PORT_3306_TCP_ADDR}

EC2にGoのアプリケーションをデプロイしてレスポンスが返ってくるまで

EC2のインスタンスが起動している前提で進めます。

ビルドする

GOOS=linux GOARCH=amd64 go build

デプロイする

ビルドしたbinaryを配置することにしました。 今回はapp以下に配置します。

scp -i ~/.ssh/hoge.pem {binary名} ec2-user@{IP}:~/app

Nginxのインストール

Nginxはリバースプロキシとして使います。

$ amazon-linux-extras | grep nginx
  4  nginx1.12                available    [ =1.12.2 ]
$ sudo amazon-linux-extras install nginx1.12

起動

$ sudo systemctl start nginx.service

自動起動

$ sudo systemctl enable nginx.service

自動起動の確認

$ systemctl is-enabled nginx.service
enabled

nginx.confの編集

80ポートで受け取って8080ポートのアプリに繋ぐようにします。

server_name  {パブリックIP};

location / {
        proxy_pass http://127.0.0.1:8080;
}

再起動します

sudo service nginx restart

EC2のセキュリティーグループの設定を変更する

EC2のインスタンス作成時にセキュリティーグループは作成されていますが、デフォルトではポート22だけ空いている状態です。
新たにHTTP 80のルールを追加します。

binary実行

cd app

./{binary名}

これでレスポンスを確認できるようになりました。

Cloud BuildでAPIドキュメントの生成を自動化

f:id:inari111:20181215134720p:plain

https://cloud.google.com/cloud-build より引用

ピックアップ Advent Calendar 2018 16日目です。
今日はサーバーサイドエンジニアのinari111がお送りします。
Goを毎日書いていますが、Goの話は出てきません… adventar.org

弊社ではProtocol BuffersというIDL(Interface Definition Language, インタフェース定義言語)を採用しています。
以前は、protocコマンドでProtoファイルからAPIのドキュメントを生成 -> GCS(Google Cloud Storage)にアップロードする作業を手動で行っていました。
手動でドキュメント生成をするのは辛く、そして忘れてしまうこともあるので、この記事ではCloud Buildを使って自動化するまでを解説したいと思います。

Cloud Buildとは

ビルド、テスト、デプロイなどを実行してくれる従量課金制なビルドサービスです。 1日あたり120分まで無料枠があります。
各ビルドステップはDockerコンテナで実行され、Dockerイメージは公式ビルダーcloud-builders-community もあるので必要に応じて使うことができます。

ファイル構成

Protoファイルはアプリケーションのソースコードとは別のリポジトリで管理していて、ファイル構成は以下のようになっています。
今回のゴールは、 proto/admin_api/*.proto proto/core/*.proto proto/user_api/*.proto のどれかに変更があったらProtoファイルからAPIドキュメントを生成し、GCSにアップロードをすることです。

.
├── Makefile
├── README.md
├── _doc
│ └── rpc (生成したドキュメントを置く)
├── _docker
│ └── Dockerfile
├── cloudbuild.yaml (ビルドステップを定義)
├── proto
│   ├── README.md
│   ├── admin_api (Admin API用Protoファイル)
│   │   └── aaa.proto
│   ├── core (Admin API, User API両方でimportするProtoファイル)
│   │   └── bbb.proto
│   └── user_api (User API用Protoファイル)
│       └── ccc.proto
└── rtdb
 └── hoge_db_schema.yaml (Realtime DatabaseのSchema)

トリガーを作成する

GitHubを選択 f:id:inari111:20181215121033p:plain

その後、対象のリポジトリの選択をします。

f:id:inari111:20181215121132p:plain

トリガーの設定を書いていきます。

f:id:inari111:20181215121331p:plain

GitHubにpushしたときに proto/admin_api proto/core proto/user_api 下のどれかのProtoファイルに変更があれば ビルドが実行されるようにしました。
proto/**/*.proto と書いても動かなかったので、下記のように記載しています。

cloudbuild.yamlを作成

cloudbuild.yamlを用意します。CircleCIのconfig.yamlみたいなものです。
各stepをこのように定義しました。

steps:
# Dockerビルド
- name: 'gcr.io/cloud-builders/docker'
  dir: "_docker"
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/{image名}', '.']

# Protoファイルからドキュメント生成
- name: 'gcr.io/$PROJECT_ID/{image名}'
  args: ['make', 'gen-proto-doc']

# GCSにアップロードする
- name: 'gcr.io/cloud-builders/gsutil'
  dir: "_doc/rpc"
  args: ['-m', 'cp', 'index.html', '${_RPC_DOC_PATH}/${BRANCH_NAME}_${SHORT_SHA}.html']

# Container Registryにpush
images: ['gcr.io/$PROJECT_ID/{image名}']

各stepを軽く解説します。
1つ目は、Dockerをビルドします。 dir で指定したディレクトリにあるDockerfileを対象とし、 gcr.io/$PROJECT_ID/{image名} というイメージを作成します。

2つ目は、前stepでビルドしたイメージを使い、 make gen-proto-doc を実行します gen-proto-doc

protoc -Iproto --doc_out=_doc/rpc --doc_opt=html,index.html proto/**/*.proto

を実行するようにMakefileに定義していて、Protoファイルからドキュメントを生成することができます。
結果として _doc/rpc/index.htmlが作成されます。

3つ目は gsutilコマンドを使ってGCSにファイルをアップロードします。 BRANCH_NAMESHORT_SHA はcloudbuild.yaml上で使うことができる変数です。デフォルトで使えるものは他にもあるのでこちらを参照してください。https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values
独自の変数をトリガーの設定ページから登録することもできます。今回はGCSのパスを_RPC_DOC_PATH として登録していました。

最後の images を書くことで Container Registryにpushできます。不要だったら記載する必要はありません。

ローカルで検証

cloud-build-localをインストール

gcloud components install cloud-build-local

cloud-build-localをインストールしたら、ローカルで実行することができます。
--configでビルドリクエストファイルを指定
--dryrun=falseでビルドを実行
--pushはビルド中に作成されたイメージをContainer Registry にpushするかどうかです
変数に _RPC_DOC_PATH を設定しているので --substitutions=_RPC_DOC_PATH=gs://{バケット名}/{パス} を指定する必要があります。
ローカルから実行する場合に限り、 BRANCH_NAMESHORT_SHA の指定も必要です。 ローカルで実行する場合はこんな感じになります。

cloud-build-local --config=cloudbuild.yaml --dryrun=false --push=false --substitutions=_RPC_DOC_PATH=gs://{バケット名}/{パス},BRANCH_NAME=hoge,SHORT_SHA=aaaaa .

↑を実行するとGCS上にアップロードできました。 f:id:inari111:20181215141458p:plain

開発時の検証やテストはcloud-build-localでだいたい事足りました。

ローカルからビルドを送信

実際にローカルからビルドを送信してリモートでビルドを実行した場合は、以下のコマンドを実行します。

gcloud builds submit --config=cloudbuild.yaml --substitutions=_RPC_DOC_PATH=gs://{バケット名}/{パス},BRANCH_NAME=hoge,SHORT_SHA=aaaaa

ブラウザからビルド実行

ブラウザからも実行可能です。

f:id:inari111:20181215142358p:plain

GitHubへのpushをトリガーにビルド実行

これは書くまでもないですね。
ビルドを実行したくなかったら、[skip ci] または [ci skip] をコミットメッセージに書くとビルドがスキップされます。

最後に

  • Cloud Buildを使い、APIドキュメントの生成を自動化することができました
  • 特定のパスのファイルに変更があった場合のみ処理を実行することができるのは便利です
  • 今回は触れませんでしたが、ドキュメントのURLをSlackに通知するのもやっていく予定です
  • 他の会社でもProtocol Buffers採用してほしい

HHKBのキーキャップをピンクに交換した

インスタの#HHKBタグをフォローしてチェックしていると、海外でキーキャップを交換している方が多く、前々から興味がありました。

Massdrop で見つけて購入を検討していたのですがいつの間にか終わってしまい、 結局AliExpressで購入しました。値段は$32.90 + 送料
海外から配送されるので到着まで10日ほど。

このようにキーキャップとキーキャップを引き抜く工具がついてきました。
(ついていないと思って工具も買っちゃっていた...)

f:id:inari111:20181008221430j:plain

まずは、キーキャップを外します。
半年近く掃除していなかったので、ついでに掃除もしました。

f:id:inari111:20181008221450j:plain

最後にキーキャップを取り付けて完了です。

f:id:inari111:20181008221505j:plain

思ったよりも派手な感じもしますが1週間も使えば慣れました。
ErgoDoxも気になり始めたしキーボードは沼だなー

favclip/testeratorを使ってGAE/Goのテストを高速化する

testeratorはGAE/Goのテストを高速化するライブラリです。
このライブラリを使ってテストの高速化ができたので紹介したいと思います。

contextを必要とするテストでは aetest.NewContext()aetest.NewInstance() を使うかと思いますが、これを使うと goapp test実行時にローカルサーバーが関数ごとに起動し、遅くなってしまいます。

func TestHoge(t *testing.T) {
    ctx, done, err := aetest.NewContext() // ローカルサーバー起動
    if err != nil {
        t.Fatal(err)
    }
    defer done()

    // 略
}

func TestFuga(t *testing.T) {
    inst, err := aetest.NewInstance(&aetest.Options{StronglyConsistentDatastore: true}) // ローカルサーバー起動
    if err != nil {
        return nil, nil, err
    }
    req, err := inst.NewRequest("GET", "/", nil)
    if err != nil {
        inst.Close()
        return nil, nil, err
    }
    ctx := appengine.NewContext(req)
    defer inst.Close()

    // 略
}

テストの数が多くなってくると、結構ストレスが溜まります。。。

ここで使うのが testeratorです github.com

testeratorはテスト間でインスタンスを使い回すことができるため、テストが高速化できるという仕組みです。
では、実際に使っていきます。

import (
    "fmt"
    "os"
    "testing"

    "github.com/favclip/testerator"
    _ "github.com/favclip/testerator/datastore"
    _ "github.com/favclip/testerator/memcache"
    _ "github.com/favclip/testerator/search"
)

func TestMain(m *testing.M) {
    if _, _, err := testerator.SpinUp(); err != nil {
        fmt.Printf("testerator.SpinUp error: %v", err)
        os.Exit(1)
    }

    status := m.Run()

    if err := testerator.SpinDown(); err != nil {
        fmt.Printf("testerator.SpinDown error: %v", err)
        os.Exit(1)
    }

    os.Exit(status)
}

func TestHoge(t *testing.T) {
    instance, ctx, err:= testerator.SpinUp()
    if err != nil {
        t.Fatal(err)
    }
    defer testerator.SpinDown()

    // 略
}

func TestFuga(t *testing.T) {
    instance, ctx, err := testerator.SpinUp()
    if err != nil {
        t.Fatal(err)
    }
    defer testerator.SpinDown()

    // 略
}

これだけです。 TestMainm.Run で各テストを実行していきます。
僕はTestMainを書くのを忘れていて、あれ...全然早くならないぞ...と少し悩んだので忘れないようにしましょう。

If you want to clean up Datastore or Search API or Memcache, You should import above packages.

とGoDocに書いてあるように、必要に応じてimportしておきましょう。

contextやinstanceを必要とする全てのテストを書き換えた結果、改善前の時間より 約30% 短縮することができました

まとめ

  • aetest.NewContext()aetest.NewInstance() はテスト関数ごとにローカルサーバーが起動するので遅い
  • testeratorでインスタンスを使い回すことでテストの時間を30%短縮できた