LLVM IR の alloca 命令のつかいかた

LLVM IR の alloca 命令の使い方について,リファレンスマニュアルに載ってない注意点があったのでメモがてら書きます.

alloca 命令とは

スタック上にメモリを確保し,確保した領域の先頭へのポインタを返します.スタック上にメモリを割り付けることでアドレスが必要なメモリ領域を得ることができ,自動変数などの実装に使うことができます.

http://llvm.org/docs/LangRef.html#alloca-instruction

%ptr = alloca i32                             ; yields i32*:ptr
%ptr = alloca i32, i32 4                      ; yields i32*:ptr
%ptr = alloca i32, i32 4, align 1024          ; yields i32*:ptr
%ptr = alloca i32, align 1024                 ; yields i32*:ptr

変数などは基本的にこの alloca 命令を使って領域を確保するお馴染みの命令です.スタック上に割り当てるので,関数を抜けると同時にその領域は解放されます.

と,ここまでならリファレンスマニュアルを読めばそれで解決なのですが,いくつか注意点があります.

alloca によるメモリ割り当てのタイミング

例えば下記のようなループを実装したいケース

int i = 0;
while (i < 10) {
    int j = i * 2;
    printf("%d\n", j);
    ++i;
}

そのまま書き下すと

  %i = alloca i32
  store i32 0 i32* %i
  br label %loop.init
loop.init:
  %i1 = load i32 i32* %i
  %0 = icmp sle i32 %i1 10
  br i1 %0 label %loop.end, label %loop.body
loop.body:
  %1 = mul i32 %i1, 2
  %j = alloca i32
  store i32 %1 i32* %j
  %2 = load i32 i32* %j
  call void @printf(i8* @str, i32 %2)
  %3 = add i32 %i1, 1
  store i32 %3 i32* %i
  br label %loop.init
loop.end:

のような IR をつくりたくなりますが,これだと10行目の %j = alloca i32 がループごとに確保されてしまうので,ループするだけで再帰でもないのにどんどんスタックを食っていってしまいます.

なので,下記のようにスタックのアロケーションは原則関数の頭でやります.

  %i = alloca i32
  %j = alloca i32
  store i32 0 i32* %i
  br label %loop.init
loop.init:
  %i1 = load i32 i32* %i
  %0 = icmp sle i32 %i1 10
  br i1 %0 label %loop.end, label %loop.body
loop.body:
  %1 = mul i32 %i1, 2
  store i32 %1 i32* %j
  %2 = load i32 i32* %j
  call void @printf(i8* @str, i32 %2)
  %3 = add i32 %i1, 1
  store i32 %3 i32* %i
  br label %loop.init
loop.end:

これは割と常識らしく,Clang は明示的にブロックをつくりこそしませんが,関数の頭で alloca を呼んでいます.Rust や Crystal は関数の最初にその関数内で必要なスタック領域をすべて alloca する基本ブロックを置く実装になっています.どちらでも良いですが,LLVMC++ API でなく C bindings な llvm-c を使う場合は後者の方が実装がかなり楽だと思います.

C89 までブロックの先頭でしか変数を定義できなかったのはこういった理由があるのかもしれません(未確認)

関数の頭でメモリを割り当てられない例外:動的サイズ配列

ここまでは一般的なケースですが,残念ながらこれではうまくいかないケースがあります.C99 の動的サイズ配列です. alloca 命令はN要素分の領域確保もできるためスタック上に動的サイズでメモリを確保すること自体は問題なくできます.

int i = 0;
while (i < 10) {
    int arr[i + 1];
    arr[i] = 42;
    printf("%d\n", arr[i]);
    ++i;
}

例えば上記のようなコードの場合,arr の確保する領域は i の値に応じて動的に変わります.なので,関数の先頭で alloca しておく前述の方法は使えません. しかし,ループの中で単に alloca してしまうとループが回るごとにスタックが積み上がっていってしまい,無限ループでもしようものならスタックを使い切ってプログラムがクラッシュしてしまうでしょう.

そこで,LLVM には llvm.stacksavellvm.stackrestore という intrinsic 関数があります.前者は現在のスタック位置をレジスタに保存し,後者はレジスタの値にスタック位置を復帰します.

  %0 = call i8* @llvm.stacksave()      ; 現在のスタック位置を %0 に保存
  %1 = alloca i32, i64 %x              ; i32 の値を %x 個分確保する
  ; %1 を使った処理
  call void @llvm.stackrestore(i8* %0) ; スタック位置を復帰.%1 の領域は解放される

これを上記の while ループのブロック内で行うことで一度消費したスタックを復帰してから次のループに臨むことができ,スタックを消費しっぱなしにならなくできます.

初めこれを Clang の出力で見た時は偶然関数の最初と最後に stacksavestackrestore があったので,どうしてこれが必要なんだろうと思いましたが,今回説明したような理由があったからでした.

Electron に Mac タッチバー API が実装された

個人的に気になっていた Electron のタッチバーサポートがついに master にマージされました.

実装は下記の PR で行われ,@MarshallOfSound さんの初期実装と @kevinsawicki さんのブラッシュアップで実装されました.

https://github.com/electron/electron/issues/8095

まだリリースされていないですが,待てなかったので master ブランチの実装を試してみました.

追記(2017/3/8): v1.6.3 beta としてリリースされました.

Electron の master ブランチをビルドする

ビルドの仕方はドキュメントにまとめられています. 再配布せず手元で試すだけであれば面倒そうな「macOS SDK」のセクションは無視してしまって大丈夫です.

# リポジトリを取得
$ git clone https://github.com/electron/electron && cd electron/
# ビルドに必要な依存関係パッケージの取得
$ ./script/bootstrap.py -v
# ビルド
$ ./script/build.py

ビルドにはそれなりに時間がかかります.out/R にリリースビルドの Electron.app が,out/Dデバッグビルドの Electron.app が生成されます.

Touchbar API

タッチバーはメインプロセスから制御することができます.レンダラプロセスからは IPC 経由でのアクセスになりそうです. macOS 10.12.1 以降のタッチバー搭載 MacBook Pro で利用できるようです

タッチバーの UI パーツにはいくつかの種類があり,各パーツごとの class と,それらをまとめる class が提供されています.

  • TouchBarButton : アイコンとテキストが置けるボタンです
  • TouchBarColorPicker : カラーピッカーです
  • TouchBarGroup : 複数の UI パーツを1つのグループとして配置できます
  • TouchBarLabel : テキストを表示できるラベルです
  • TouchBarPopover : アイコンとラベルとキャンセルボタンが表示できる,タッチバーに出るダイアログボックス的な UI です
  • TouchBarSlider : 音量などに使えるスライダーです
  • TouchBarSpacer : タッチバーのアイテムとアイテムの間にスペースを開けることができます.開けるスペースは small, large, flexible (スペースを取れるだけ取る) の中から選べます
  • TouchBar : TouchBar は上記 Touchbar の UI パーツを並べてタッチバーインスタンスを生成できます

タッチバーインスタンスBrowserWindowsetTouchbar() メソッドによってセットできます.セットされると即座にタッチバーが表示されます.また,setTouchbar メソッドに null を与えることでタッチバーを消すことができます.

サンプルアプリ

ドキュメントにのっているサンプルアプリを動かしてみます.ドキュメントにはコード片のみなので,動くコードを下記に置いてみました.

github.com

サンプルアプリではタッチバー上でスロットを回すことができます.

www.youtube.com

エントリポイントである main.js は下記の通りです.

const path = require('path')
const {app, BrowserWindow, TouchBar} = require('electron')

// 必要なタッチバーの UI パーツをインポートする
const {TouchBarLabel, TouchBarButton, TouchBarSpacer} = TouchBar

let spinning = false

// スロットの各リールの文字列を表示するラベル3つ
const reel1 = new TouchBarLabel()
const reel2 = new TouchBarLabel()
const reel3 = new TouchBarLabel()

// スロットを回した結果を表示するラベル
const result = new TouchBarLabel()

// 'Spin' ボタン
const spin = new TouchBarButton({
  label: '🎰 Spin', // ボタンのテキスト.ここでは絵文字だけどアイコンも置けるはず…
  backgroundColor: '#7851A9',
  click: () => {
    // スロットが回っている間はクリックを無視する
    if (spinning) {
      return
    }

    // スロットを回す処理
    spinning = true
    result.label = ''

    let timeout = 10
    const spinLength = 4 * 1000 // 4 seconds
    const startTime = Date.now()

    // クリックされてから4秒間スロットを回す
    const spinReels = () => {
      // スロットのリールを更新する.止まった時のリールが結果になる
      updateReels()

      if ((Date.now() - startTime) >= spinLength) {
        finishSpin()
      } else {
        // 1.1倍ずつスロットを遅くしていく(慣性っぽさ)
        timeout *= 1.1
        setTimeout(spinReels, timeout)
      }
    }

    spinReels()
  }
})

// ランダムに絵柄を1つ選ぶ関数
const getRandomValue = () => {
  const values = ['🍒', '💎', '7️⃣', '🍊', '🔔', '⭐', '🍇', '🍀']
  return values[Math.floor(Math.random() * values.length)]
}

// 3つのリールを更新
const updateReels = () => {
  reel1.label = getRandomValue()
  reel2.label = getRandomValue()
  reel3.label = getRandomValue()
}

const finishSpin = () => {
  // Set を使って重複を省き,残った要素数で絵柄がいくつ合ったかをチェックする
  const uniqueValues = new Set([reel1.label, reel2.label, reel3.label]).size

  if (uniqueValues === 1) {
    // すべての絵柄が揃ったとき:大当たり!
    result.label = '💰 Jackpot!'
    result.textColor = '#FDFF00'
  } else if (uniqueValues === 2) {
    // 3つ中2つのリールが同じ絵柄だったとき:当たり
    result.label = '😍 Winner!'
    result.textColor = '#FDFF00'
  } else {
    // 絵柄がまったく揃わなかったとき:はずれ
    result.label = '🙁 Spin Again'
    result.textColor = null
  }

  spinning = false
}

// [ボタン]  [r1] [r2] [r3]  [result]
//
// のように配置.配列に入れたパーツが入れた順にタッチバーに表示される
const touchBar = new TouchBar([
  spin,
  new TouchBarSpacer({size: 'large'}),
  reel1,
  new TouchBarSpacer({size: 'small'}),
  reel2,
  new TouchBarSpacer({size: 'small'}),
  reel3,
  new TouchBarSpacer({size: 'large'}),
  result
])

let window

app.once('ready', () => {
  window = new BrowserWindow({
    width: 200,
    height: 200
  })
  window.loadURL(`file://${path.join(__dirname, '/index.html')}`)
  // ブラウザウィンドウにタッチバーをセットする.
  // このタイミングでタッチバーにスロットマシンが表示される
  window.setTouchBar(touchBar)
})

// 全てのウィンドウが閉じたときアプリを閉じる
app.on('window-all-closed', () => {
  app.quit()
})

まとめ

ついに Electron アプリでもタッチバーにアクセスできるようになりました. まだ入ったところなので多少バグなどがあるかもしれませんが,面白い機能なので是非自分がつくっているアプリでも活用を考えたいと思います. 何事も無ければ次リリースに含まれると思うので,今週後半か来週あたりには v1.6 系で使えるようになるのではと思います.

GitHub のハイライトがおかしい時のなおしかた(GitHub のハイライトの仕組み)

GitHub でコードを見ていると,ハイライトがおかしいのを見つけることがたまにあります.ここではその直し方を紹介します.

次の2パターンを想定しています.

  • 自分のリポジトリのあるファイルのハイライト言語がおかしい
  • 構文ハイライトが間違えている,言語の新機能に対応していない

自分のリポジトリのあるファイルのハイライト言語がおかしい

コードをどの言語でハイライトするかは自動判別されますが,実はある程度ユーザ側で制御する方法があります.

VimEmacs のモードラインを書く

ファイル単位で VimEmacs の設定をマジックコメントとして書けるモードラインですが,実は GitHub もそれを参照しています

// C++ だけど拡張子は .h … C じゃなくて C++ でハイライトしてほしい

// vim: set ft=cpp
//
// または
//
// -*- mode: cpp;-*-

これで上記のコードは C++ として認識されるはずです.

.gitattributes に指定する

リポジトリ.gitattributes ファイルにハイライトを指定することもできます.

$ cat .gitattributes
*.h linguist-language=C++

またハイライト以外の設定もできて,

$ cat .gitattributes
vendor/* linguist-vendored
docs/*.rb linguist-documentation=false

例えば上記の設定をすると,vendor/ 配下は他のプロジェクトのコードと判断してリポジトリの stat (リポジトリページのヘッダにあるコード率など)から除外できたり,docs/*.rb はドキュメント向けのコードとして,stat カウントから除外できたりします.

構文ハイライトが間違えている,言語の新機能に対応していない

実は GitHub のコードハイライトを行うライブラリは OSS で公開されています.

https://github.com/github/linguist

このリポジトリでは

など様々なメタデータが管理されています.なので例えば新しい言語をつくって十分に普及してきたら,ここに自分の言語を追加したプルリクを出すなどもできるわけですね.

今回は一番最後の言語のハイライト方法を見ます.

GitHub のハイライトは .tmbundle という,TextMate の構文ハイライトフォーマットまたは,Atom の cson を使った構文ハイライトフォーマットを利用してハイライトされています.これらはオープンソースで開発されているものを利用しており,一覧は下記のディレクトリにあります.

https://github.com/github/linguist/tree/master/vendor/grammars

サブモジュールでリビジョン固定されていますが,何らかのタイミングで更新されているので,対象の .tmbundle.cson を管理しているリポジトリにプルリクを投げて修正してもらえばいずれ linguist の依存する構文ハイライターもアップデートされ,GitHub 上のハイライトがなおるという仕組みです.

例えば僕は以前 Vim script の try ... finally がハイライトされていないことに気づいたので,それを追加するプルリクを投げました.

また,上記リポジトリでは各種フレームワーク対応(例えば jquery.*.jsjQuery のファイルなど)も行われているので,各フレームワーク向けのハイライトを改善したい時は YAML の設定ファイル にプルリクを出せば良さそうです.

GitHub でコードを読んでいるとハイライトが壊れていて困ることがあるかもしれませんが,実は自力で修正できますというご紹介でした.

公式ドキュメント

https://github.com/github/linguist/blob/master/docs/overrides.md