低レイヤを知りたい人のための Cコンパイラを Rust で作っている

hkdnet.hatenablog.com

↑の続きというわけで Rust 版を作っている

github.com

Rust 化して気づいたのは Vec なる tokens を consume していくような形にすると毎回食べ終わった Vec を返す必要がありそこそこだるいということだった。これは Vec は固定的なものであるというように捉えて index だけ返すようにしたほうがいいかもしれない。

また、Node について binary operator を考えると当然 Node<Node, Node> のようなものにならざるを得ないのだが1、この場合に Node が再帰的に定義されてしまうため、sizeof(Node) が一意に定まらないという問題があることがわかった。この場合に Rust 的には Box をもつようにして heap 領域に確保したメモリへのポインタをもつことで stack として確保すべき領域を決定的にしているということがわかった。おもしろい。確かにCで書くときも *Node みたいなメンバ宣言にするしなあ。

Rust 特有のアレコレについて悩みながらもやっている。特にCが書けるようになりたいわけでもないので2このまま Rust 版を育てるかもしれない。


  1. なお実際には NodeType にもたせている

  2. 別に読めればいい

『低レイヤを知りたい人のための Cコンパイラ作成入門』感想

低レイヤを知りたい人のための Cコンパイラ作成入門

インターネット上で公開されている範囲については実装が終わりました。

github.com

内容について

非常によかったです。わかりやすかったです。僕は技術書の場合、だいたい冒頭だけ読んで、実装してみて、本の内容と比較するという読み方をしています。この本については、ある機能を実装するにあたって、「このへんは今はやらなくていいや」と割り切るのが上手いと感じました。実装していると「これもやらなきゃじゃん」「こうなってるほうがいいよなー」みたいな気持ちがどんどん生まれますが、実際にを読んでみると「これは後ででいいです」とバッサリ。これはCコンパイラを複数回作った経験とコンピューターサイエンスに関する体系的な知識があるがゆえにできているのかなーと思っています。

このコンテンツを読む前に期待している点としては、「言語実装に関する理解が深まること」と「アセンブリ機械語を生成するフェイズの知識が得られること」の2点でした。やってみた感じとしては、前者は特に深まったなーと思っています。tokenize, parse というフェイズごとに何をしているのかという点や実際のバイナリをつくるまでの過程というのが理解できました。

一方で後者は、いまだによくわかりません。吐くアセンブリの意味を正しく理解できているかというとあやしいし……。これはどちらかというと文章としての問題ではなく、読み手としてアセンブリと仲良くなる努力がまだ足りてないのかなーと思っています。どうにかしないとなー。いい教材みたいなのがあると助かります。fizzbuzzみたいなのを自分で書いてみるか。

これから

上記のアセンブリと仲良くなるっていうのと、あとはCで書くのがダルいので他言語で実装しなおすかもしれません。あるいはもうちょい自力ですすめるか。あと解析フェイズの理解が進んだせいで TaPL の言語実装が適当すぎることに気づいたのでそっちを直したくもなっています。どうしよう。

動いているようす

こんな感じになります。ちゃんと動いてますね。中間生成物のアセンブリもチラ見せ。

$ make test
bin/hkdcc -test
OK
./test.sh
0; => 0
42; => 42
1+2; => 3
1+10+2; => 13
3-1; => 2
23-12; => 11
3 - 1; => 2
70+2*5*5; => 120
4/2; => 2
5+6*7; => 47
5*(9-6); => 15
(3+5)/2; => 4
1; 2; => 2
a = 1; => 1
a = 1; a; => 1
a = 1; a + 1; => 2
a = b = 1; a + b; => 2
1 == 1; => 1
a = 1; 1 == a; => 1
a = 2; 1 == a; => 0
a = 2; a == 1; => 0
a = 2; a == a; => 1
a = 1 == 1; a; => 1
a = 0 == 1; a; => 0
1 != 1; => 0
2 != 1; => 1
OK
$ cat tmp.s
.intel_syntax noprefix
.global _main
_main:
  push rbp
  mov rbp, rsp
  sub rsp, 208
  push 2
  push 1
  pop rax
  pop rdi
  cmp rdi, rax
  setne al
  movzx rax, al
  push rax
  pop rax
  mov rsp, rbp
  pop rbp
  ret

signal.Notify したチャンネルを close すると死ぬ(可能性がある)

TL; DR

signal.Stop しよう

https://golang.org/pkg/os/signal/#Stop

検証

ダメな例

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    close(c)

    fmt.Println("C-c plz")
    time.Sleep(10 * time.Second)
}
$ go run main.go
C-c plz
^Cpanic: send on closed channel

goroutine 5 [running]:
os/signal.process(0x10dbc60, 0xc00002aa30)
        /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal.go:227 +0x163
os/signal.loop()
        /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go:23 +0x52
created by os/signal.init.0
        /usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go:29 +0x41
exit status 2

ある channel がクローズ済かは送信者はわからないので仕方がない。

大丈夫な例

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    signal.Stop(c) // signal.Stop を追加した
    close(c)

    fmt.Println("C-c plz")
    time.Sleep(10 * time.Second)
}
$ go run main.go
C-c plz
^Csignal: interrupt

OK

背景

graceful restart 的なことをするときに、シグナルでハンドリングをしていた。のだが、場合によっては即座に死んでほしくないパターンがあった。API通信相手がメンテ中だとわかっている場合とかは即死すると即死→メンテ→即死→メンテの無限ループって怖くね?状態になる。

しょうがないのでメンテのときはある程度待ってから死ぬようにしてみるか、と思って書いているわけだが、シグナルハンドリングが終わったあとの息が長くなるとリソースの解放漏れが気になってきた。そこでさくっと close しようとしたら、これ死ぬんじゃね?って思って調べて今に至る。

ここまで書いた後でさっさと死んで起動直後にメンテ中か確認すれば特に悩む必要なかったなって思った。おしまい。

オフラインリアルタイムどう書く E29 の回答 (Golang)

yhpg.doorkeeper.jp

問題 : http://nabetani.sakura.ne.jp/hena/orde29unes/
実装リンク集 : https://qiita.com/Nabetani/items/f2db9b916c0a301b744f

勝ちました。珍しく Golang で。

なんかかっこいいやり方あんのかなーと思ったけど、1文字ずつ素直に読んでけばいいやって感じに。こういうの1文字ずつ読んでなんかの区切りで返すのって lexer だよねって思ってそんな名前にしております。TaPLの言語を実装している経験が役にたっている。

l.cur += 2 のところは slash の mode を用意すればいいということを他の人の回答から学んだ。なるほど。

byte リテラルの書き方がわからなくてなんかアレな感じになっている。

package e29

import (
    "errors"
    "fmt"
    "strings"
)

type lexer struct {
    cur  int
    text string

    mode lexerMode
}
type lexerMode int32

const (
    normal lexerMode = iota
    singleQuote
    doubleQuote
)

var slashByte, dQuoteByte, sQuoteByte byte
var validChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"'/")

const noAnswer = "-"

type result struct {
    output string
    err    error
}

func init() {
    tmp := "/\"'"
    slashByte = tmp[0]
    dQuoteByte = tmp[1]
    sQuoteByte = tmp[2]
}

func solve(input string) result {
    l := lexer{text: input}
    var tokens []string
    for l.Next() {
        token, err := l.NextToken()
        if err != nil {
            return result{output: noAnswer, err: err}
        }
        tokens = append(tokens, token)
    }
    if l.mode != normal {
        return result{output: noAnswer, err: errors.New("unterminated quote")}
    }
    if len(tokens) == 0 {
        return result{output: noAnswer, err: errors.New("no entries")}
    }
    output := strings.Join(tokens, ",")
    return result{output: output, err: nil}
}

func (l *lexer) Next() bool {
    if l.cur == len(l.text) {
        return false
    }
    return true
}

func isValidByte(b byte) bool {
    for _, bb := range validChars {
        if b == bb {
            return true
        }
    }
    return false
}

func (l *lexer) NextToken() (string, error) {
    var ret []byte
    beg := l.cur
loop:
    for {
        if l.cur >= len(l.text) {
            return "", errors.New("idx")
        }
        c := l.text[l.cur]
        if !isValidByte(c) {
            return "", fmt.Errorf("invalid char: %v", string(c))
        }
        switch c {
        case slashByte:
            if l.mode == normal {
                if l.cur+1 == len(l.text) { // 終わり
                    return "", errors.New("should not end with /")
                }
                if l.text[l.cur+1] == slashByte {
                    // スラッシュ2個のパターン
                    ret = append(ret, c)
                    l.cur += 2
                } else {
                    l.cur++
                    break loop
                }
            } else {
                // quote 中は / として扱う
                l.cur++
                ret = append(ret, c)
            }
        case dQuoteByte:
            if l.mode == normal {
                l.mode = doubleQuote
                l.cur++
            } else if l.mode == doubleQuote {
                l.mode = normal
                l.cur++
            } else if l.mode == singleQuote {
                l.cur++
                ret = append(ret, c)
            }
        case sQuoteByte:
            if l.mode == normal {
                l.mode = singleQuote
                l.cur++
            } else if l.mode == doubleQuote {
                l.cur++
                ret = append(ret, c)
            } else if l.mode == singleQuote {
                l.mode = normal
                l.cur++
            }
        default:
            l.cur++
            ret = append(ret, c)
        }

        if l.cur == len(l.text) {
            break loop
        }
    }
    entry := string(ret)
    if entry == "" {
        return "", fmt.Errorf("entry should not be blank %d-%d", beg, l.cur)
    }
    return entry, nil
}

Rails で全部のモデルについて何かの処理をしたい

eager_load = true にした上で ↓ のスクリプトbundle exec rails r する

ar_classes = []

ObjectSpace.each_object do |obj|
  next if obj.class.name.to_s != 'Class'
  if obj.ancestors.include?(ActiveRecord::Base) && obj != ActiveRecord::Base
    ar_classes << obj
  end
end

# 本体の処理

今回は実際にはテーブル名がほしかったのでこんな処理を。

table_names = ar_classes.map do |klass|
  klass.respond_to?(:table_name) && klass.table_name
end

puts table_names.compact.sort

SwitchPoint 使ってるとそれの子供が釣れるみたいだけど気にしない。


完全にせやな、であった

読了: 『ドラゴンクエストXを支える技術ーー 大規模オンラインRPGの舞台裏』

読み終えました。ざっと俯瞰する感じで面白かった。とても深いところがあったりとかめっちゃ参考になる〜みたいな本ではないです。タイトルどおり、まさに舞台裏を教えてくれる本という感想でした。

自分のバックグラウンドとしてはweb系の業務システムをやってきていて、webで継続的にサービスを提供することについてはある程度理解がある。一方でゲーム系は全くやっていないので全然わからない。本書ではコンシューマーゲームの売り切りをやってきた人が多い中でMMORPGをサービスとして提供していくことについて描かれている点が多かったように感じた。そういった対比は、サービス提供するということの特徴の再確認と、売り切りゲームの特徴を学ぶという点でなかなかおもしろかった。

3Dの計算などの負荷量はあまりよくわからないのだが、負荷系ではスレッドは活用しないという判断をした点、全部永続化はきついのでKVSを併用してる点などが記憶に残っている。といってもそうだろうねえというくらいの感想でしかないが。

ゲームをつくっていくという点については Luaスクリプトとの二段構成というのが実際どういうもんなのかというのがなんとなく理解できてよかった。大規模ゲームを支えているだけあってツール類が手厚いのもなるほど、という感じがあった。

macOS Mojave + nasm の Hello world

nasm の Hello world 記事はそこそこあるもののコピペで動かなかったのでメモ。

TL;DR

  • エントリポイントは start ではなくて _main という名前にしておく
  • ld コマンドに -lSystem-macosx_version_min 10.13 を指定しておく

前提

  • macOS Mojave 10.14.1
  • uname -m -> x86_64

書いたやつ

github.com

GLOBAL _main

SECTION .text

_main:
  mov rax, 0x2000004;
  mov rdi, 1;
  mov rsi, hello_world;
  mov rdx, 13;
  syscall;
  mov rax, 0x2000001;
  mov rdi, 0;
  syscall;


SECTION .data;
  hello_world db "Hello World", 0x0a;
hello.out: hello.o
    ld -lSystem -o hello.out -macosx_version_min 10.13 hello.o
hello.o: hello.asm
    nasm -f macho64 -o hello.o hello.asm

インターネット上にいろいろ落ちてるサンプルとの変更点は1つだけ。GLOBAL でアクセス可能にしているシンボル名が start になっているのが多かったのだが、_main にした。 これは ld コマンド実行時にシンボルが見つからないんだが?と怒られたので変更。たぶん macOS 側の仕様が変わった?のかな?

Undefined symbols for architecture x86_64:
  "_main", referenced from:
     implicit entry/start for main executable
ld: symbol(s) not found for inferred architecture x86_64

また make コマンドにも -lSystem-macosx_version_min 10.13 を追加。これもわからなくてググったのだが以下のSOにたどり着いた。そしたらこいっちでも「 start から _main に変えてみたがまだだめなんだが」とのこと。start って名前はどこから来てるんですかねえ。

https://stackoverflow.com/questions/52830484/nasm-cant-link-object-file-with-ld-on-macos-mojave

$ ./hello.out
Hello World