koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

Vimからfzfを利用する

はじめに

fzfとは,percolpecoと同様,絞り込みの検索を行うことのできるコマンドラインツールである. 日本では,percolやpecoが有名で,fzfはあまり有名ではないが,海外では有名であるらしい. fzfはVimから利用できるように,公式のリポジトリAPIを提供するVimプラグインが付属している. この記事では,fzfをVimから使う方法について述べる.

fzfの特徴

fzfはGoで実装されている. Goで実装されているから,マルチプラットフォームなのかと思ってしまうが,実は Windowsでは利用することができないCygwin上では利用可能).

fzfを利用したプラグインの作り方

正直,fzfのREADME.mdwikiのサンプルを見るのが早いのだが,それではこの記事の意味が無いので,ちゃんと書く. (日本語で書いておくと,日本人が読みやすいという利点もあるだろうし)

まず,fzfをVimから利用するためには,fzfが提供しているプラグインruntimepath を通す必要がある. これはfzfをクローンした場所にもよるが,

$ git clone https://github.com/junegunn/fzf.git ~/.fzf

としたならば,

set rtp+=~/.fzf

とするだけでよい. すなわち,fzfのルートディレクトリを runtimepath に追加するだけでよい. 上記の場合,fzfのVimプラグイン本体は ~/.fzf/plugin/fzf.vim にあるので,Vimからfzfの関数を利用できるようになるわけだ.

fzfをVimから利用するのは簡単で, fzf#run() に渡す適切な辞書を定義するだけだ. 後は,その辞書を第一引数とし, fzf#run() をコールするだけで,Vimからfzfを利用できる. 例えば,wikiには,

nnoremap <silent> <Leader>C :call fzf#run({
    \ 'source': map(split(globpath(&rtp, "colors/*.vim"), "\n"),
    \         "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
    \ 'sink': 'colo',
    \ 'options': '+m',
    \ 'left': 30
    \})<CR>

というfzfを用いるキーマッピングの例が紹介されている. これは,利用可能なcolorschemeを候補として表示し,選択したcolorschemeに変更するというものだ.

この fzf#run() に渡す辞書のキーと値には以下のようなものが有効である. 同じキーであっても,値の型によって挙動が異なる点に注意しよう.

  • source
    • 値の型が 文字列 のとき,その文字列を外部コマンドとして実行し,その結果をfzfに渡す
    • 値の型が リスト のとき,このリストをfzfに渡す
  • sink
    • 値の型が 文字列 のとき,選択した候補に対してその文字列をVimコマンドとして実行する
      • 候補は文字列の末尾に結合される(例:値が 'edit' ならば,実行されるコマンドは 'edit [選択したもの]'
    • 値の型が 関数参照 のとき,選択した候補を指定した関数(への参照)の第一引数として渡し,関数をコール
      • 指定する関数参照の引数は1つでなければならない
    • 複数の候補が選択されたとき,それぞれの候補に対して, sink で指定したアクションを1回1回実行する
  • sink*
    • 値の型は 関数参照 のみで,選択した候補を指定した関数(への参照)の第一引数として渡し,関数をコール
    • 複数選択された候補を リストとして扱う
      • すなわち,選択された候補はリスト化されて,関数の第一引数に渡される
  • options
    • 値の型は 文字列 のみで,fzfの起動オプションを指定する
  • dir
    • 値の型は 文字列 のみで,fzf起動時の作業ディレクトリを指定する
  • updownleftright
    • 値の型は 数値 または 文字列
      • 数値型では行数,または列数を指定できる( 20 など)
      • 文字列型では行,または列の占める割合を指定できる( '50%' など)
      • 文字列型の場合,末尾に % を付けないと,行数,または列数の指定になる( '50' では50行,または50列で50%でない )
    • tmux上でVimを起動している場合のみ
  • window
    • neovimのみ
    • 値の型は 文字列 のみで,fzfのウィンドウを開くコマンドを指定する(例: vertical aboveleft 30new
  • launcher
    • gvimのみ
    • 値の型が 文字列 のとき,fzfを起動する外部のターミナルエミュレータ名を指定する
    • 値の型が 関数参照 のとき,その関数の返り値をfzfを起動する外部のターミナルエミュレータとして用いる

キーとして必ず持つべきものは,候補に対するアクションを担当する sink または sink* である. 候補の取得を担当する source は,後述するが,ほぼ必須の項目である. なお, sinksink* の2つがある場合, sinksink* の順番に処理がなされる.

ちなみに, source を指定しない場合,デフォルトのfzfの動作(再帰的なファイル検索)になるので,単純にファイルを検索したいのであれば, source を敢えて指定しないというのも手だ. 単純なファイラとして用いるのであれば,以下のようにするとよいだろう.

call fzf#run({'sink': 'edit'})

sink も指定しなかった場合,候補を選択しても何のアクションも行われない.

" カレントディレクトリ以下のファイルが再帰的に表示されるが,
" 候補を選択しても,何のアクションも行われない
call fzf#run({})

プラグインに組み込む

僕の場合,fzfをプラグインに導入する場合,実装を別ファイルに分割している. unite.vimctrlp.vimに習い,分割したファイルは autoload/fzf/ に配置することにする.

以下は,カレントディレクトリのファイルを表示し,選択されたファイルの行数を echo するという単純なサンプルである.

  • plugin/sample.vim
if exists('g:loaded_sample')
  finish
endif
let g:loaded_sample = 1
let s:save_cpo = &cpo
set cpo&vim

command! FZFSample  call fzf#run(fzf#sample#option())

let &cpo = s:save_cpo
unlet s:save_cpo
  • autoload/fzf/sample.vim
let s:save_cpo = &cpo
set cpo&vim

function! fzf#sampleoption() abort
  return {
        \ 'down': 20,
        \ 'sink': function('s:sink'),
        \ 'source': s:gather_candidates()
        \}
endfunction

function! s:gather_candidates() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

autoload/fzf/sample.vimfzf#sample#option() で返却する辞書の一部の項目は,いちいち新しく定義し直す必要は無いので,以下のように記述してもよいだろう.

let s:save_cpo = &cpo
set cpo&vim

let s:option = {
      \ 'down': 20
      \}
function! s:option.sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


function! fzf#sample#option() abort
  let s:option.source = s:gather_candidates()
  return s:option
endfunction


function! s:gather_candidates() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

この例では,候補の取得をVim Script側で行っているが,カレントディレクトリのファイルを検索するのなら,外部コマンドを用いてもよいだろう. fzfがコマンドラインツールであることを考えれば,そちらの方が好ましいかもしれない.

let s:save_cpo = &cpo
set cpo&vim

let s:option = {
      \ 'down': 20,
      \ 'source': 'find -maxdepth 1 -find f'
      \}
function! s:option.sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


function! fzf#sample#option() abort
  return s:option
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

複数選択したい場合

先述の例では触れていないが,fzfの拡張は複数選択できるようにすることが可能である. 複数選択はfzf自体の機能にあるので, options にfzfのオプション -m ,または --multi を指定するだけでよい. 先述の例であれば,以下のように s:option を変更するだけだ.

let s:option = {
      \ 'down': 20,
      \ 'options': '-m'
      \}
function! s:option.sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction

このとき, sink に指定したアクションが,選択した各候補に対して実行される. もし,複数選択したものを,Vim Script側ではリストとして扱いたいならば, sink* を利用するとよい.

function! s:sink(candidates) abort
  for candidate in a:candidates
    echo len(readfile(candidate))
  endfor
endfunction

let s:option = {
      \ 'down': 20,
      \ 'options': '-m',
      \ 'sink*': function('s:sink')
      \}

感想

fzfはあくまで外部ツールであって,Vimからfzfに操作を移さなければならない点が残念に思えた. 僕としては,外部ツールはあくまでシームレスに用いることができるようにしてほしいと思っている. ただ,tmux上のVimで用いた場合に,候補選択用の新しいペインを作成する点や,neovimに対応している点など,なかなか面白い試みをしていると感じた.

また,複数選択に対応している点も魅力的だと感じた. 候補選択型インターフェースで,ユーザが作成する拡張で,複数選択ができるものを作ることができるのは,実質unite.vimとfzfぐらいのものではないかと思われる(ctrlp.vimは特殊な場合に限り,複数選択可能).

参考

ctrlp.vimのエクステンションの作り方

はじめに

この記事はctrlpvim/ctrlp.vimのエクステンションの作り方についての記事であり,ctrlpの公式リポジトリのextensionsブランチに書かれているエクステンションの作り方,および以下の3つの記事の内容に個人的な知見を追加したものである.

CtrlPとは

ctrlpvim/ctrlp.vimは候補絞り込み検索型のファイラである. Shougo/unite.vimと双璧を成すプラグインとして,とても有名である.

オリジナルはkien氏のものであるが,kien氏と連絡が取れないことから,それをforkしたctrlpvim/ctrlp.vimが誕生した.

ctrlp.vimは標準機能であるファイラだけではなく,候補絞り込み検索のインタフェースを利用して,ctagsのタグやundo履歴を候補として表示し,選択された場合にそれに応じたアクションが実行される拡張が本体に付属している.

このctrlp.vimの拡張は,第三者が作成することもできる. unite.vimの様々な拡張(unite source)が多数の人により開発され公開されているように,ctrlp.vimの拡張(以後,エクステンションと呼ぶ)も多数の人により開発され公開されている.

ctrlp.vimのエクステンションは,unite.vimのsourceほど自由に作ることはできないが,シンプルかつ簡単に作ることが可能な点が魅力であると思っている.

2015 / 12 / 01 追記

kien氏と連絡が取れ,2015年11月30日にkien氏のリポジトリではメンテナンスされていないということがオリジナルの方のREADME.mdに明記された

エクステンションの作り方

g:ctrlp_ext_vars というリストを保持するグローバル変数があり,このグローバル変数に作成するエクステンションに関する辞書を追加するだけで,エクステンションの作成は終わりである. そして,関数 ctrlp#init() に適切なインデックスを渡すことで,作成したエクステンションを利用することができる.

ただし,作成するエクステンションは,1つのファイルにし, autoload/ctrlp/sample.vim に置く. リスト g:ctrlp_extensions にエクステンションを登録してある場合,ctrlp.vim本体の読み込み時に,ctrlp.vim本体が runtime autoload/ctrlp/sample.vim として読み込みを行うので,エクステンションのファイルは autoload/ctrlp/ 以下に置かなければならない.

何はともあれ,実例を見てもらうとしよう. 以下は,カレントディレクトリのファイルを表示し,選択されたファイルの行数を echo するというシンプル(かつ何の役にも立たない)なエクステンションのサンプルである.

  • plugin/sample.vim
if exists('g:loaded_sample')
  finish
endif
let g:loaded_sample = 1
let s:save_cpo = &cpo
set cpo&vim

command! CtrlPSample  call ctrlp#init(ctrlp#sample#id())

let &cpo = s:save_cpo
unlet s:save_cpo
  • autoload/ctrlp/sample.vim
if exists('g:loaded_ctrlp_sample') && g:loaded_ctrlp_sample
  finish
endif
let g:loaded_ctrlp_sample = 1
let s:save_cpo = &cpo
set cpo&vim

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  let g:ctrlp_ext_vars = add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
let s:id = g:ctrlp_builtins + len(g:ctrlp_ext_vars)


function! ctrlp#sample#id() abort
  return s:id
endfunction


function! ctrlp#sample#init() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! ctrlp#sample#accept(mode, str) abort
  call ctrlp#exit()
  echo len(readfile(a:str))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

先に述べたように,ctrlp.vim本体が runtime autoload/ctrlp/sample.vim として,作成したエクステンションを読みにいくので,autoloadファイルであっても,ファイルの先頭に二重読み込み防止の記述をしておく必要がある.

このサンプルは,koturn/ctrlp-sampleにあるので,インストールして動作を確認したり,改造したりして,ctrlp.vimのエクステンションの作り方の勉強に役立てていただきたい. なお,masterブランチのものは,後述する僕の好みに合わせてコードを洗練してあるので,上記のコード例そのままのものが欲しいなら,not-refiedブランチのものを参照していただきたい.

g:ctrlp_ext_vars に追加する辞書のそれぞれのキー

エクステンションの作成において,要となるのが, g:ctrlp_ext_vars に追加する辞書である. ここでは,その辞書が持つべきキーについて述べる.

必須

  • init : 文字列(evalすると関数呼び出しになる文字列)
    • 評価されるべき関数呼び出しの文字列( 'ctrlp#sample#init()' など)
    • 候補の取得に用い,関数から候補のリストを返却する
    • unite.vimでいうところの gather_candidates
    • CtrlPのバッファに移動した後に呼び出される
  • accept : 文字列(候補が選択されたときに呼び出される関数名)
    • 選択した候補に対するアクションを定義した関数の名前を渡す
    • initenterexit とは異なり, 関数呼び出しの括弧は不要
    • ここで指定した関数のAPIctrlp#sample#accept(mode, str) といった形にすること(仮引数名は何でもよいが,本体付属のエクステンションに従った)
      • mode : 候補の選択に入力したキーに応じた文字列が入っている.対応は以下の通り
        • <CR> : 'mode' は 'e'
        • <C-v> : 'mode' は 'v'
        • <C-t> : 'mode' は 't'
        • <C-x> : 'mode' は 'h'
      • str : 選択した候補
    • 通常,関数の先頭で ctrlp#exit() をコールするとよい
    • mode に応じて処理を変更しているエクステンションは少ない
  • lname : 文字列(省略無しのエクステンション名 - long name)
    • ステータスラインに表示される省略無しのエクステンション名
  • sname : 文字列(省略したエクステンション名 - short name)
    • ステータスラインに表示される省略されたエクステンション名
  • type : 文字列で 'line''path''tabs''tabe' のいずれか
    • 'line'
      • それぞれの候補に対し,末尾まで入力文字列とのマッチを行う
    • 'path'
      • それぞれのファイルパスのような形式の候補に対し,末尾まで入力文字列とのマッチを行う
      • それぞれの候補について,入力にマッチした部分がハイライト される
    • 'tabs' (tab start)
      • 最初のTAB文字までマッチする
      • 候補の文字列の最初のTAB以降を,ユーザに情報として提示したい場合に用いる(候補に関するヒントの提示)
    • 'tabe' (tab end)
      • 最後のタブ文字までマッチする
      • 用途は 'tabs' と同じで,どのTABまでをマッチ対象にするかが異なる

任意

  • enter : 文字列(evalすると関数呼び出しになる文字列)
    • CtrlPが起動する前に呼び出される処理
    • 元のバッファの filetype の取得することなどに用いる
  • exit : 文字列(evalすると関数呼び出しになる文字列)
    • CtrlPが終了する前に呼び出される処理
  • opts : 文字列(evalすると関数呼び出しになる文字列)
    • オプションの設定などに用いるらしい
    • enter で指定した関数より前に呼び出される
  • sort : 数値( 01
    • 候補をソートするか否か
    • 0 なら候補をソートしない(デフォルト), 1 なら候補をソートする
  • specinput : 数値( 01 ) - special input
    • ..@cd といった特殊な入力を可能にする
      • :h ctrlp-input-formats を参照
    • 0 なら特殊入力を不可に(デフォルト), 1 なら特殊入力を可能にする
    • ..../ と等価で,1つドットを増やす度にディレクトリ階層を1つ上にした入力として解釈され,リターンキーを押すと,ワーキングディレクトリを変更し,再度 init に指定した関数が呼び出され,候補が更新される
      • ... なら ../../.... なら ../../../ に移動する
    • @cd はワーキングディレクトリを変更する
      • 候補選択時に @cd ./plugin/ と入力して,リターンキーを押すと,ワーキングディレクトリを ./plugin/ に変更し,再度 init に指定した関数が呼び出され,候補が更新される
    • CtrlP起動中にワーキングディレクトリを変更したい場合に用いる
    • 候補の絞り込みに用いるのではない
    • ワーキングディレクトリが候補の取得に関わっているものに有効

ドキュメント化されていないもの(任意)

  • nolim : 数値( 01 ) - no limit
    • 0 なら制限あり(デフォルト), 1 なら無制限(スクロール可能)に
    • 日本語などの検索しづらい候補があるときに用いると便利かもしれない
    • mattnさんのctrlpのエクステンションに多く用いられているのを見掛ける
  • opmul : 数値( 01 ) - open multiple
    • 0 なら複数選択不可(デフォルト), 1 なら複数選択可能に
    • 掛け算記号のことではない
    • 複数選択は候補上で <C-z> と入力することで候補にマークを付け, <C-o> を入力することで行うことができる
    • ただし, 複数選択のアクションは "基本的に" 定義できず,ファイルを開くという動作のみ である
      • 存在するファイルのパスに限定されるが,autocmdBufReadCmd を用いることで強引にアクションを定義することは可能(危険)
    • 候補はファイルパスとして解釈され,存在しないファイルパスである候補は選択しても除外される
    • 基本的に定義しない.本体付属のプラグインに用いられている
  • wipe : 文字列(関数名)
    • <F7> で候補を除去するときのアクションを指定する
      • opmul1 のとき,<C-z> でマークを付け,<F7> でマークを付けた候補を指定した関数に渡す
      • マークが1つも付いていない場合,<F7> で全ての候補を除去するかどうかのメッセージが表示される
        • o を入力すると,指定した関数に空リストを渡してコールし,その後ctrlpバッファに戻る
        • c を入力すると,候補を除去せず,ctrlpバッファに戻る
    • initenterexit とは異なり, 関数呼び出しの括弧は不要
    • ここで指定した関数のAPIctrlp#sample#wipe(entries) といった形にすること(仮引数名は何でもよいが,本体付属のエクステンションに従った)
    • 新たな候補リスト (引数の除去対象が除去されたリストが想定されている)を関数から返却する
    • 基本的に定義しない.本体付属のプラグインに用いられている

opmulwipe を用い,指定した候補を除去したいのであれば,以下のようにするのがよいだろう.

function! ctrlp#sample#init() abort
  " 候補を他の関数で用いるために,スクリプトローカル変数で保持しておく
  " この s:candidates は ctrlp#sample#exit() でunletするとよい
  let s:candidates = filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
  return s:candidates
endfunction

function! ctrlp#sample#wipe(entries) abort
  return empty(a:entries) ? [] : filter(s:candidates, 'index(a:entries, v:val) == -1')
endfunction

上記の ctrlp#sample#wipe() が分かりにくいのであれば,以下のようにforを用いて書くとよい.

function! ctrlp#sample#wipe(entries) abort
  if empty(a:entries)
    return []
  endif
  for entry in a:entries
    let idx = index(s:candidates, entry)
    if idx != -1
      call remove(s:candidates, idx)
    endif
  endfor
  return s:candidates
endfunction

前者の filter() を用いたものと比べると, a:entriess:candidates の捉え方(ループの主体)が異なっているが,候補に重複が無ければ同一の結果が得られるはずだ. (前者は除去候補にマッチしたものを候補から全て取り除き,後者は候補のうち,最初にマッチしたもののみを取り除く)

Vim scriptのパースは時間がかかる(ループ毎にもパースを行っているはず)ため, s:candidates の数が多くないのであれば,前者の方が高速に動作すると思われる.

CtrlP起動時にワーキングディレクトリを変更する

ctrlp#init() に作成したエクステンションのIDを渡すことで,CtrlPを起動するようになっていることは最初に述べた. 実は, ctrlp#init() は,第二引数に辞書を取ることもでき,その辞書のキー dir の値に,ワーキングディレクトリを指定することで,CtrlP起動時のディレクトリを変更することができる. これは相対パスからファイルを簡単に辿りたい場合などに使用できる.

command! -nargs=1 -complete=dir CtrlPSample  call ctrlp#init(ctrlp#sample#id(), {'dir': <q-args>})

候補のシンタックスハイライト

本体付属のエクステンションやこの記事を参考にすると,CtrlPの候補にハイライトを適用する(すなわち,CtrlPバッファ内でハイライトを行う)には, ctrlp#sample#init() 内で,

function! ctrlp#sample#init() abort
  call ctrlp#hicheck('CtrlPSampleTabExtra', 'Comment')
  syntax match CtrlPSampleTabExtra '\zs\t.*$'
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

などのようにするとよい. 本体付属のエクステンションでは,シンタックスハイライト用の設定を行う部分を関数として切り出し,

function! ctrlp#sample#init() abort
  call s:syntax()
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:syntax() abort
  if ctrlp#nosy()
    return
  endif
  call ctrlp#hicheck('CtrlPSampleTabExtra', 'Comment')
  syntax match CtrlPSampleTabExtra '\zs\t.*$'
endfunction

としている( ctrlp#nosy() については後述する).

ctrlp#hicheck() はハイライトのリンクをチェックする関数であり,リンクが存在しないならば,リンクを行う. 実装は非常に単純で,

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#hicheck(grp, defgrp)
    if !hlexists(a:grp)
        exe 'hi link' a:grp a:defgrp
    en
endf

となっている.

また, ctrlp#nosy() (no syntax) はシンタックスハイライトが利用できる環境であるかどうかを判別する関数である. シンタックス機能が利用できないVimや, :syntax enable をしていないVimならば 1 を返却し,シンタックスハイライトが可能ならば, 0 を返却する.

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#nosy()
    retu !( has('syntax') && exists('g:syntax_on') )
endf

グローバル変数 g:syntax_on は, :syntax enable とコマンドを実行する(大抵,.vimrcに書かれているだろう)ことで定義される変数で, :syntax off とコマンドを実行すると unlet される. 大抵のユーザはシンタックス機能をオンにしているので,わざわざチェックする必要も無いが,本体付属のプラグインに沿った形に記述しておくと,雰囲気は良いだろう.

シンタックスハイライトの用途としては, typetabs を指定した場合,最初のタブ文字以降を目立たない色にするといったものが考えられる. 実は,先述のハイライトの例は,本体付属の typetabs であるエクステンションにも書かれている「タブ文字以降のハイライトをコメントと同じものにして,目立たなくする」ものである.

改良案

ここまではctrlp.vimのエクステンションの作り方について述べたが,ここからは「エクステンションのコードはこう書くのが好きだ」という個人の意見を書く.

ctrlp.vim本体のグローバル変数の参照

ctrlp.vimの拡張はプラグインとしてオプショナルであり,NeoBundle等で依存関係を明示したくない場合があるかもしれない(そんなユーザがいるのかは疑問であるが). このことについて考えているときに,以下の記事を見て,なるほど,と感じた.

ctrlp.vimのExtensionを書くときに:NeoBundleLazy autoloadを考慮してg:ctrlp_builtinsをそのまま使わない - cafegale

ctrlp.vimの拡張はあくまでオプショナルとしてプラグインに付属させており,NeoBundleに依存関係が記述されておらず,ctrlp.vimが遅延読み込みされるように設定してある場合,先述の記事にあるように,作成したプラグインを読み込みしている段階で g:ctrlp_builtins が定義されていないという問題が起こることもあるだろう. 従って,ctrlpが提供しているautoload関数 ctrlp#getvar() を用いて,ctrlp.vim本体の読み込みを発生させ,ctrlpのグローバル変数が読み込まれるようにした方が望ましいはずだ. そのことを踏まえ,作成するエクステンションのファイルの先頭に,

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

と記述し, g:ctrlp_builtins の代わりに, s:ctrlp_builtins を用いるようにするとよさそうだ.

ただ,プラグインマネージャである neobundle.vim のためだけの記述をするというのは,本質的ではなく,少し嫌な気持ちになる. この g:ctrlp_builtins の取得方法に関しては,個人の好みが大きいだろう. 正直,ユーザがNeoBundleの依存関係をキッチリ書けば問題の無い話でもある.

ちなみに, ctrlp#getvar() は以下のような実装になっている.

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#getvar(var)
    retu {a:var}
endf

a:var は文字列と想定されるので,{a:var} は, eval(a:var) と読み替えてよく, a:val を評価した結果に置き換えられる. すなわち, ctrlp#getvar() は, autoload/ctrlp.vim 内で引数を評価した結果を返す関数であり,autoload/ctrlp.vimスクリプトローカル変数を取得することもできる.

g:ctrlp_ext_vars へのエクステンションの辞書の追加

Vimプラグインの拡張機能プラグインを作ってVimをさらに使いやすくしよう - 29th Sta. という記事の例では,

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  let g:ctrlp_ext_vars = add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif

としているが, add() は第一引数のリストに対して破壊的変更を加えるので,返却値を再び第一引数のリストに代入する必要はない(どちらが読みやすいかは置いておくとして). また,スクリプトローカル変数 s:sample_var を保持する必要もない. これらのことから,上記コードは以下のように記述することができる.

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  call add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
unlet s:sample_var

個人的にはif文を用いずにシンプルに書きたいので,以下のように記述することにしている.

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})

add() の第一引数が即値 [] となることもあるので,この場合は add() の返却値を利用しなければならない.

可能な限り,グローバル関数を使用しないようにする

エクステンション側にグローバル関数を定義し,ctrlp.vimに渡す辞書にそのグローバル関数名を渡しているが,必ずしもそうする必要はない. スクリプトローカル関数であっても,スクリプト番号付きであれば,グローバルに参照することが可能だからだ.

function! s:hoge() abort
  echomsg 'Hello, World'
endfunction
" :call <SNR>18_hoge() などという形で,グローバルに呼び出すことが可能

このことを踏まえ, g:ctrlp_ext_vars に辞書を追加する記述を,以下のような記述に変更した.

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:sid_prefix

僕は,不特定多数の相手(プラグインであったり,.vimrcであったり)にAPIを提供する場面では,グローバル関数を用い,今回のように作成したプラグインから 能動的に 他のプラグインに自身の関数を渡す場合では,スクリプトローカル関数を用いるとよいだろうと考えている. ただ,ここまでしてグローバル関数を嫌う必要があるのかと問われれば,どうなんだろうと思ってしまう. また,スクリプト番号の取得は,コードとしてややごちゃごちゃしており(5行は必要になる),グローバル関数を用いる方がスマートという気もするが,この場では気にしないことにする.

ここまでのことを踏まえ,最初に紹介したカレントディレクトリのファイルを表示し,選択されたファイルの行数を表示するエクステンションを以下のように書き直した.

if exists('g:loaded_ctrlp_sample') && g:loaded_ctrlp_sample
  finish
endif
let g:loaded_ctrlp_sample = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_var = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#sample#id() abort
  return s:id
endfunction


function! s:init() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  echomsg len(readfile(a:str))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

ctrlp.vimのエクステンションのテンプレート

僕はctrlp.vimのエクステンションを作成する際に,以下のテンプレートを用いている. <+FILEBASE+> を適宜,エクステンションのファイル名からディレクトリ部と,拡張子を取り除いたものにすればよい( :%s/<+FILEBASE+>/\=fnamemodify(expand('%'), ':t:r') ). これを thinca/vim-template などのテンプレートプラグインと組み合わせると,サクッとエクステンションを作ることができるはずだ.

なお,以下のテンプレートには,CtrlPバッファでハイライトするコードは含まれない. 僕の場合,ハイライトが必要なエクステンションを書くことがあまり無いので,必要に応じて追加するようにしているためだ.

if exists('g:loaded_ctrlp_<+FILEBASE+>') && g:loaded_ctrlp_<+FILEBASE+>
  finish
endif
let g:loaded_ctrlp_<+FILEBASE+> = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_var = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': '<+FILEBASE+>',
      \ 'sname': '<+FILEBASE+>',
      \ 'type': 'line',
      \ 'enter': s:sid_prefix . 'enter()',
      \ 'exit': s:sid_prefix . 'exit()',
      \ 'opts': s:sid_prefix . 'opts()',
      \ 'sort': 0,
      \ 'specinput': 0,
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#<+FILEBASE+>#id() abort
  return s:id
endfunction


function! s:init() abort
  " Gather candidates
  let candidates = []
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  " Write actions
endfunction

function! s:enter() abort
  " Called before s:init()
  " For example: get filetype
endfunction

function! s:exit() abort
  " Called when exit ctrlp
endfunction

function! s:opts() abort
  " Called before s:enter()
  " Set options etc...
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

最小限の機能に絞ったテンプレートならば,以下のようになるだろう.

if exists('g:loaded_ctrlp_<+FILEBASE+>') && g:loaded_ctrlp_<+FILEBASE+>
  finish
endif
let g:loaded_ctrlp_<+FILEBASE+> = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': '<+FILEBASE+>',
      \ 'sname': '<+FILEBASE+>',
      \ 'type': 'line',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#<+FILEBASE+>#id() abort
  return s:id
endfunction


function! s:init() abort
  " Gather candidates
  let candidates = []
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  " Write actions
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

基本的にこれらのテンプレートの s:init()s:accept() を中心に実装するだけでよいはずだ.

最後に

ctrlp.vimのエクステンションを作るにあたって覚えなければならないことは,unite.vimより少なく,unite sourceより簡単に作ることができる. 日本人が作ったプラグインには,unite sourceが付属しているものが多いが,ctrlp.vimのエクステンションが付属しているプラグインは少ない. unite sourceを付属させるのであれば,是非ともctrlp.vimのエクステンションも付属させてみてはどうだろうか?

追記

2015 / 12 / 01

何とあのmattnさんにこの記事を取り上げていただいた!

当初は割とパパッと書いた記事で,恥ずかしい間違いなどもあったのだが,まさかmattnさんに紹介していただけると思っていなかった.

mattnさんはブログ中で, g:ctrlp_ext_vars に追加するときに if ~ else を用いるか,get() を用いるかについて堀下げて解説してくださっている. 特に,get() の第一引数に g: という一見変数っぽくないものを与えるあたりについて解説してくださっている.

let s:sample_var = {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  call add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
unlet s:sample_var

とするより,

let g:ctrlp_ext_vars = get(g:, 'ctrlp_ext_vars', []) + [{
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
\}]

とした方が上級Vimmerに見られるようなので,みなさんも要チェックだ.

ちなみに,Vim scriptはパースがネックである部分もあるので,前者より後者の方が高速に動作するはずだ. カッコよさの他,マシンに優しくしたり,電力消費を僅かに抑えたいのであれば,後者を用いるとよいだろう. (1度しか実行されない処理に何を言っているんだ,というツッコミは無しでお願いしたい)

参考

Windowsでneovimを使う

注意(2016/10/18 追記)

この記事の情報は古くなっているので、新しい記事を参照してください.

前書き

僕自身,neovim自体にはあまり注目していなかった人間であるが,neovimが頑張ってるっぽく,かなり話題を耳にするようになったので,敢えてWindowsでneovimを試すことにした. Windowsで試すきっかけとなったのは,Osaka.vim #6にて,「Windows用のneovimのバイナリって無いんですかね?」と聞いたところ,公式で配布されており,Wikiにもそのことが書かれているという情報をいただいたからである. また,(ゴミ)プラグイン作成者としても,自分のプラグインがneovimで動作するかどうかには興味があった.

ダウンロード

公式でneovim本体のバイナリとQtで実装されたフロントエンドのバイナリが配布されているので,それらをダウンロードしてくる. 公式のWikiに書かれている通り,

  • neovimのAppVeyorからneovim本体のバイナリをダウンロードする(手順は以下とgifアニメを参照).
    • Environment: GENERATOR=Visual Studio 14 Win64, DEPS_PATH=deps64 というラベルをクリック.
    • Artifacts タブをクリック.
    • Neovim.zipをダウンロードし,解凍.
  • Releases · equalsraf/neovim-qtより,neovim-qt.zipをダウンロードし,解凍.
  • Neovim\nvim.exeneovim-qt\ に移動.
  • Neovim\share\nvim\runtime\ 以下にあるファイルを全て neovim-qt\ にぶちまける.

f:id:koturn:20151111194729g:plain

という手順を踏むとよい. あとは,nvim-qt.exe をダブルクリックすると,neovimが起動する. neovim本体である nvim.exe 単体ではneovimを起動できない(Windowsのコンソールをサポートしていない)ので注意すること.

自分でビルドしたい場合

AppVeyorのビルドログを参考にし,以下の順番でコマンドを実行するとよい. ただし,cmake,git,python2.7以上,Visual Studio 2015(2015以上であることが必須)などがインストールされており, cl.exe などのツールがコマンドプロンプトで使用できることが前提である.

:: MSVCでのビルド用のリポジトリ/ブランチを持ってくる
> git clone -q --branch=tb-mingw https://github.com/equalsraf/neovim.git
> git checkout -qf 27f0fa51e9ca886c87159ee1e2c394426d64d128
> cd neovim
:: 環境変数の設定
> set GYP_MSVS_VERSION=2015
> set GENERATOR=Visual Studio 14 Win64
> set DEPS_PATH=deps64
:: サードパーティライブラリのビルド
> mkdir %DEPS_PATH%
> cd %DEPS_PATH%
> cmake -G "%GENERATOR%" ..\third-party\
> set PATH=%PATH%;C:\cygwin64\bin\
> cmake --build .
> cd ..
:: neovimのビルド
> mkdir build
> cd build
> cmake -G "%GENERATOR%" -DCMAKE_BUILD_TYPE=Debug -DDEPS_PREFIX=..\%DEPS_PATH%\usr -DCMAKE_INSTALL_PREFIX=..\INSTALL ..
> cmake --build .

ある程度の開発環境が整っているならば,これでビルドすることができるはずである. 現在のところ,本家のリポジトリを持ってくるわけではないが,将来的には本家から持ってくるようにできるのではないだろうか?

なお,Cygwinバイナリがあるディレクトリのパスを通していると,思わぬところでつまづいたりするので注意すること. neovim-qtのビルドについてはやっていない.(ただのフロントエンドだろうし...)

neovimの設定ファイル

最近になって,Vim%HOME%\_vimrcLinux環境では ~/.vimrc)にあたるファイルは %HOME%\.nvimrc から %XDG_CONFIG_HOME%\nvim\init.vimに,%HOME%\vimfiles\Linux環境では ~/.vim/)にあたるディレクトリは %HOME%\.nvim\ から %XDG_CONFIG_HOME%\nvim\ に変更された.

Linux環境における $XDG_CONFIG_HOME のデフォルトディレクトリ( $XDG_CONFIG_HOME 環境変数が存在しない場合のディレクトリ)は, ~/.config/ であるが,Windowsでは, %AppData% がデフォルトディレクトリとなるらしい(参考). 僕は,Linuxと同様に %XDG_HOME_CONFIG%%AppData% ではなく, %HOME%\.config とした.

既存のVimが用いる .vimrcvimfiles\ をneovimの設定として扱いたい場合,以下のようにしてシンボリックリンクを作成するとよい. (ただし,コマンドプロンプトを管理者権限で起動する必要があるかもしれない. また,環境変数 %XDG_CONFIG_HOME%%HOME% がちゃんと設定され,共に存在することを確認しておこう.)

> mklink /D %XDG_CONFIG_HOME%\nvim %HOME%\vimfiles\
> mklink %HOME%\vimfiles\init.vim %HOME%\_vimrc

init.vimに関しては,シンボリックリンクを作成するのではなく,以下のように記述するのもよいだろう.

source ~/_vimrc

この場合, init.vim にneovimだけで用いる設定を記述することが可能である.

init.vim は新規に書くことが望ましいが,僕は4000行を超える .vimrc を記述しており,neovimの init.vim 用に .vimrc を書き直すようなことはしたくなかったので, .vimrc にneovimの設定も記述することにした. neovimの判定は has('nvim') という式を用いるとよい. neovimを利用している環境であるならば,この式は 1 と評価されるはずである.

注意点

僕が軽くWindowsのneovim + neovim-qtを触って遭遇した問題は,以下のようなものである.

  • フロントエンドであるneovim-qtの問題であると思われるが,文字コードutf-8以外のファイルを開くと文字化けする.
  • neovim側から新規ファイルを作成しようとすると(存在しないファイルを編集しようとすると),"Permission denided" となってしまい,強制的に保存しないといけない(:write! を用いる).
  • コンパイル時にいくつかのオプションを無効化しているので,既存の _vimrc を用いるとエラーが発生した.
    • 例えば,:language コマンドは使用できなかった.
  • neovim本体をフロントエンドであるneovim-qtを用いているので, has('gui_running')0 となることを期待していたが,起動時は 0.起動後は 1 を返却するようになっていた.
  • 当然のことであるが,一部のプラグインはうまく動作しない
    • bling/vim-airline を導入して, :split:vsplit などとして画面を分割すると,盛大にエラー吐いたなど ....

感想

neovimの公式が配布しているWindows向けのバイナリには難が多いと感じた. Linuxで自分でビルドした場合,エンコーディング等の問題もなく,快適に使うことができたので,現状ではWindows対応がややおろそかであると思われる. また, :terminal コマンドが現状では利用できなかったのが少し残念であった.

参考

WhitespaceをC言語ソースに変換する

はじめに

WhitespaceとはBrainfuckやLazyKと同じ難解プログラミング言語と言われる言語のひとつである. 難解プログラミング言語ジョーク言語と言われるが,実装の容易さやシンプルな言語仕様を考えると,とても興味深い言語である.

Brainfuckチューリングマシンに毛がはえた程度の言語であるが,Whitespaceはスタックマシン型の処理系を想定した言語である. そして,スタック操作,算術命令,ヒープ操作,フロー制御,入出力機能を持ち,比較的高い水準でのプログラミングが可能である.

BrainfuckC言語に変換するという例は世の中にあふれ返っており,変換するコードをいくつも見掛けた. 天下のWikipediaにも変換方法は記述されている.

命令 対応するC言語コード
> ptr++;
< ptr--;
+ (*ptr)++;
- (*ptr)--;
. putchar(*ptr);
, *ptr = getchar();
[ while (*ptr) {
] }

しかし,WhitespaceをC言語に変換するという例は見掛けない. そこで,今回はWhitespaceをいかにC言語に変換するかについて書こうと思う.

Whitespaceの言語仕様

まず,Whitespaceの言語仕様を復習しようと思う. どうも簡単にググって出てきた日本語記事で紹介されているWhitespaceの仕様は少し古いものが多く,公式サイトで紹介されているものと異なっている. (日本語Wikipediaにも古い仕様が書かれている) 例えば,以下のサイトには,古いWhitespaceの仕様を記述していたり,古い仕様の処理系を置いてあったりする.

以下の表の命令欄には,

  • [Space] -> S
  • [Tab] -> T
  • [改行] -> L

として,Whitespaceのコードを書いてある. VM Code欄には,後半の説明で便宜的に利用するVM命令の名称と考えてもらうとよい. そして,動作欄にはその命令の動作を書いている.

命令 VM Code 動作
SS[NUMBER] STACK_PUSH 引数に指定された値をスタックにプッシュする(即値のプッシュ)
STS STACK_DUP_N スタックの上からn番目をコピーし,スタックの一番上に積む
SLS STACK_DUP スタックの上から1番目をコピーし,スタックの一番上に積む
STL STACK_SLIDE スタックの上から1番目をキープしつつ,上から2番目から(2 + 引数)番目を捨てる
SLT STACK_SWAP スタックの上から1番目と2番目を交換する
SLL STACK_DISCARD スタックの上から1番目を捨てる
TSSS ARITH_ADD スタックの上から1番目と2番目をポップし,それを加算(2番目 + 1番目)した結果を最上段にプッシュする
TSST ARITH_SUB スタックの上から1番目と2番目をポップし,それを減算(2番目 - 1番目)した結果を最上段にプッシュする
TSSL ARITH_MUL スタックの上から1番目と2番目をポップし,それを乗算(2番目 * 1番目)した結果を最上段にプッシュする
TSTS ARITH_DIV スタックの上から1番目と2番目をポップし,それを除算(2番目 / 1番目)した結果を最上段にプッシュする
TSTT ARITH_MOD スタックの上から1番目と2番目をポップし,その剰余(2番目 % 1番目)をとった結果を最上段にプッシュする
TTS HEAP_STORE スタックの上から1番目と2番目を取り出し,2番目をヒープのアドレスとし,そこに1番目の値を書き込む(heap[2番目] = 1番目
TTT HEAP_LOAD スタックの上から1番目を取り出し,1番目をヒープのアドレスとし,そのアドレスの値をスタックの最上段にプッシュする( stack_push(heap[1番目]);
LSS[LABEL] 引数に指定されたラベルを定義する
LST[LABEL] FLOW_GOSUB 引数に指定されたラベルをサブルーチンとして呼び出す(呼び出し位置を記憶してジャンプ)
LSL[LABEL] FLOW_JUMP 引数に指定されたラベル位置にジャンプする
LTS[LABEL] FLOW_BEZ スタックの上から1番目を取り出し,その値が0ならば引数に指定されたラベル位置にジャンプする
LTT[LABEL] FLOW_BLTZ スタックの上から1番目を取り出し,その値が0より小さいならば引数に指定されたラベル位置にジャンプする
LTL FLOW_ENDSUB サブルーチンの呼び出し元にジャンプする(関数のreturn)
LLL FLOW_HALT プログラムを終了する
TLSS IO_PUT_CHAR スタックの上から1番目を取り出し,その数値に対応するascii文字を出力
TLST IO_PUT_NUM スタックの上から1番目を取り出し,その数値をascii文字として出力
TLTS IO_READ_CHAR スタックの上から1番目を取り出し,その値をヒープのアドレスとし,そこに標準入力から1文字読み込む( heap[1番目] = gethar();
TLTT IO_READ_NUM スタックの上から1番目を取り出し,その値をヒープのアドレスとし,そこに標準入力から数値を読み込む( scanf("%d", &heap[1番目]);

上記の表を見てわかるように,命令はスタック操作,算術命令,ヒープ操作,フロー制御,入出力の5種類に大別でき,それぞれの命令毎にプレフィックスが決まっている.

命令の種類 プレフィックス
スタック操作 S
算術命令 TS
ヒープ操作 TT
フロー制御 L
入出力 TL

よく使う命令ほど,少ない文字で済むように文字が割り当てられている.

Whitespace処理系では,後方へのラベルジャンプをサポートする必要があるため,一度コード全体コンパイルをすることによって,ラベルが定義されている位置を知っておかなければならない. (コンパイル時にラベルの定義は処理するので,「ラベルを定義する」というVM命令は必要ない)

Whitespaceにおける「引数」とは,正規表現[ \t]\+$ にマッチするものと考えておくとよい. 数値はSを0,Tを1とした2進数表記で表現する. 例えば,STTST ならば,2進数: 01101 なので,数値13となる. ラベルはSとTの連続させて表現する.8文字用いて1文字のasciiコードで表現する必要はないが,そのようにしているサンプルコードもある.

Whitespace to C

WhitespaceからC言語に変換するにあたって,サブルーチン呼び出しが問題となった(他の命令については,解説する必要はないだろう). サブルーチン呼び出し(FLOW_GOSUB)はgoto(FLOW_JUMP)と違い,呼び出し元を記憶し,呼び出し終了時(FLOW_ENDSUB)に,記憶していた呼び出し元に戻る必要がある. つまり,C言語でいうところの関数呼び出しを実現しなければならない.

Brainfuckのループのように,ジャンプ先と戻り位置が1対1で対応しているならば問題はないが,サブルーチンは複数箇所から呼び出されるため,戻り位置の候補が複数あることになる. また,Whitespaceには「サブルーチン定義」という命令はなく,サブルーチン呼び出しは単なる呼び出し位置を記憶したラベルジャンプにすぎない. ジャンプ先のラベルがただのジャンプ命令に利用される可能性もあるし,ラベルより前の処理から継続して,ラベル内の処理に突入する可能性もある. そのため,サブルーチンに対応する関数を定義するという手段も使えない. 以上の理由により,サブルーチン呼び出しを単純な方法でC言語に変換することはできない.

そこで, gotosetjmp()longjmp() を用いることにより,サブルーチン呼び出しを実現した. setjmp() は簡単に言うと,呼び出し位置の情報を,引数の jmp_buf 型の変数に保存する関数である. longjmp() は,引数に指定された jmp_buf 位置に戻る. これと goto を組み合わせることで,簡易的なサブルーチン呼び出しが実現できるというわけだ.

具体的には,次のコードのようにC言語コードに変換する.

/* ... */
printf("static jmp_buf call_stack[CALL_STACK_SIZE];\n");
printf("static size_t stack_idx = 0;\n");

/* for (vmcodes : vmcode) { のようなコードでループする */
  switch (vmcode) {
    /* ... */
    case FLOW_GOSUB:
      /* LABEL = read_label(); のような感じでLABELを用意しておく */
      printf(
        "  if (!setjmp(call_stack[call_stack_idx++])) {\n"
        "    goto %s;\n"
        "  }\n", LABEL);
      break;
    case FLOW_ENDSUB:
      printf("  longjmp(call_stack[--call_stack_idx], 1);");
      break;
      /* ... */
  }
/* } */

これでサブルーチン呼び出しを実現できた. ラベルに関しては,SpaceをS,TabをTに置換したラベル名で変換先のC言語コード中に定義する.

SSSTSSTT:

まとめ

WhitespaceからC言語への変換は,サブルーチン呼び出しが鬼門であったが, gotosetjmp()longjmp() を用いることで実現できた.

なお,インタプリタ/C言語へのトランスレータの機能を持つWhitespace処理系は,koturn/Whitespaceに置いてある.

$ make

とすれば簡単にビルドできる. MSVCであっても,

> nmake /f msvc.mk

とすればビルドできるはずだ. そして,以下のように実行すると,WhitespaceからC言語へ変換できる.

$ ./whitespace [Whitespace source file] -t -o out.c

WhitespaceからC言語での変換例

最後に,koturn/Whitespaceを用いた変換例を記載する. このプログラムはWhitespaceのインタプリタ,Whitespaceコードのニーモニック表現,およびC言語ソースへの変換機能を有している. ここで,何となくWhitespaceからC言語への変換のイメージが掴めると思う.

なお,Whitespaceのコードは視認できないので,前述と同じように

  • [Space] -> S
  • [Tab] -> T

とする. ただし,改行文字はそのまま改行を行う文字とする.

変換元のWhitespaceソースコード

SSSS
SSSTSSTSSS
TTSSSST
SSSTTSSTST
TTSSSSTS
SSSTTSTTSS
TTSSSSTT
SSSTTSTTSS
TTSSSSTSS
SSSTTSTTTT
TTSSSSTST
SSSTSTTSS
TTSSSSTTS
SSSTSSSSS
TTSSSSTTT
SSSTTTSTTT
TTSSSSTSSS
SSSTTSTTTT
TTSSSSTSST
SSSTTTSSTS
TTSSSSTSTS
SSSTTSTTSS
TTSSSSTSTT
SSSTTSSTSS
TTSSSSTTSS
SSSTSSSSS
TTSSSSTTST
SSSTTSTTTT
TTSSSSTTTS
SSSTTSSTTS
TTSSSSTTTT
SSSTSSSSS
TTSSSSTSSSS
SSSTTTSSTT
TTSSSSTSSST
SSSTTTSSSS
TTSSSSTSSTS
SSSTTSSSST
TTSSSSTSSTT
SSSTTSSSTT
TTSSSSTSTSS
SSSTTSSTST
TTSSSSTSTST
SSSTTTSSTT
TTSSSSTSTTS
SSSTSSSST
TTSSSSTSTTT
SSSS
TTSSSSS

STSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST

STSTTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST




SSSTTSSSSTSTTSSTSSSTTSSTSS
TSSS
T

SSSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST
S
STTTS
S
TSSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
T
SSSSST
TSSS
S
STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST

SSSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
S

S


T

SSSTTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS
S
SS
ST
TSTTTS
SSSSTSTS
TSST
TSSTTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
S

SSST
TSSS
S
STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS

SSSTTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
S

SSST
TSSSSSSS
TTS
T

SSSTTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST
SSSTSTS
SSSTTST
T
SST
SS
T
  • 実行例
$ ./whitespace.out hworld.ws
Hello, world of spaces!

ニーモニック表現

上記Whitespaceコードを擬似的なVM命令のニーモニック表現で表現したものである. C言語への変換に用いるわけではないが,イメージを掴む助けになるだろう. なお,ジャンプは絶対アドレス指定である.

0000: STACK_PUSH 0
0005: STACK_PUSH 72
0010: HEAP_STORE
0011: STACK_PUSH 1
0016: STACK_PUSH 101
0021: HEAP_STORE
0022: STACK_PUSH 2
0027: STACK_PUSH 108
0032: HEAP_STORE
0033: STACK_PUSH 3
0038: STACK_PUSH 108
0043: HEAP_STORE
0044: STACK_PUSH 4
0049: STACK_PUSH 111
0054: HEAP_STORE
0055: STACK_PUSH 5
0060: STACK_PUSH 44
0065: HEAP_STORE
0066: STACK_PUSH 6
0071: STACK_PUSH 32
0076: HEAP_STORE
0077: STACK_PUSH 7
0082: STACK_PUSH 119
0087: HEAP_STORE
0088: STACK_PUSH 8
0093: STACK_PUSH 111
0098: HEAP_STORE
0099: STACK_PUSH 9
0104: STACK_PUSH 114
0109: HEAP_STORE
0110: STACK_PUSH 10
0115: STACK_PUSH 108
0120: HEAP_STORE
0121: STACK_PUSH 11
0126: STACK_PUSH 100
0131: HEAP_STORE
0132: STACK_PUSH 12
0137: STACK_PUSH 32
0142: HEAP_STORE
0143: STACK_PUSH 13
0148: STACK_PUSH 111
0153: HEAP_STORE
0154: STACK_PUSH 14
0159: STACK_PUSH 102
0164: HEAP_STORE
0165: STACK_PUSH 15
0170: STACK_PUSH 32
0175: HEAP_STORE
0176: STACK_PUSH 16
0181: STACK_PUSH 115
0186: HEAP_STORE
0187: STACK_PUSH 17
0192: STACK_PUSH 112
0197: HEAP_STORE
0198: STACK_PUSH 18
0203: STACK_PUSH 97
0208: HEAP_STORE
0209: STACK_PUSH 19
0214: STACK_PUSH 99
0219: HEAP_STORE
0220: STACK_PUSH 20
0225: STACK_PUSH 101
0230: HEAP_STORE
0231: STACK_PUSH 21
0236: STACK_PUSH 115
0241: HEAP_STORE
0242: STACK_PUSH 22
0247: STACK_PUSH 33
0252: HEAP_STORE
0253: STACK_PUSH 23
0258: STACK_PUSH 0
0263: HEAP_STORE
0264: STACK_PUSH 0
0269: FLOW_GOSUB 282
0274: FLOW_GOSUB 367
0279: FLOW_HALT
0280: ARITH_ADD
0281: FLOW_ENDSUB
0282: STACK_DUP_N 0
0287: HEAP_LOAD
0288: STACK_DUP_N 0
0293: FLOW_BEZ 310
0298: IO_PUT_CHAR
0299: STACK_PUSH 1
0304: ARITH_ADD
0305: FLOW_JUMP 282
0310: STACK_POP
0311: STACK_POP
0312: FLOW_ENDSUB
0313: STACK_DUP_N 0
0318: STACK_DUP_N 0
0323: IO_READ_CHAR
0324: HEAP_LOAD
0325: STACK_DUP_N 0
0330: STACK_PUSH 10
0335: ARITH_SUB
0336: FLOW_BEZ 353
0341: STACK_POP
0342: STACK_PUSH 1
0347: ARITH_ADD
0348: FLOW_JUMP 313
0353: STACK_POP
0354: STACK_PUSH 1
0359: ARITH_ADD
0360: STACK_PUSH 0
0365: HEAP_STORE
0366: FLOW_ENDSUB
0367: STACK_PUSH 10
0372: STACK_PUSH 13
0377: IO_PUT_CHAR
0378: IO_PUT_CHAR
0379: FLOW_ENDSUB

変換後のC言語ソースコード

#include <assert.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>

#ifndef __cplusplus
#  if defined(_MSC_VER)
#    define inline      __inline
#    define __inline__  __inline
#  elif !defined(__GNUC__) && (!defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L)
#    define inline
#    define __inline
#  endif
#endif

#define STACK_SIZE 65536
#define HEAP_SIZE 65536
#define CALL_STACK_SIZE 65536

#define LENGTHOF(array) (sizeof(array) / sizeof((array)[0]))
#define SWAP(type, a, b) \
  do { \
    type __tmp_swap_var__ = *(a); \
    *(a) = *(b); \
    *(b) = __tmp_swap_var__; \
  } while (0)

inline static int  pop(void);
inline static void push(int e);
inline static void dup_n(size_t n);
inline static void slide(size_t n);
inline static void swap(void);
inline static void arith_add(void);
inline static void arith_sub(void);
inline static void arith_mul(void);
inline static void arith_div(void);
inline static void arith_mod(void);
inline static void heap_store(void);
inline static void heap_read(void);

static int stack[STACK_SIZE];
static int heap[HEAP_SIZE];
static jmp_buf call_stack[CALL_STACK_SIZE];
static size_t stack_idx = 0;
static size_t call_stack_idx = 0;


int main(void)
{
  push(0);
  push(72);
  heap_store();
  push(1);
  push(101);
  heap_store();
  push(2);
  push(108);
  heap_store();
  push(3);
  push(108);
  heap_store();
  push(4);
  push(111);
  heap_store();
  push(5);
  push(44);
  heap_store();
  push(6);
  push(32);
  heap_store();
  push(7);
  push(119);
  heap_store();
  push(8);
  push(111);
  heap_store();
  push(9);
  push(114);
  heap_store();
  push(10);
  push(108);
  heap_store();
  push(11);
  push(100);
  heap_store();
  push(12);
  push(32);
  heap_store();
  push(13);
  push(111);
  heap_store();
  push(14);
  push(102);
  heap_store();
  push(15);
  push(32);
  heap_store();
  push(16);
  push(115);
  heap_store();
  push(17);
  push(112);
  heap_store();
  push(18);
  push(97);
  heap_store();
  push(19);
  push(99);
  heap_store();
  push(20);
  push(101);
  heap_store();
  push(21);
  push(115);
  heap_store();
  push(22);
  push(33);
  heap_store();
  push(23);
  push(0);
  heap_store();
  push(0);
  if (!setjmp(call_stack[call_stack_idx++])) {
    goto STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST;
  }
  if (!setjmp(call_stack[call_stack_idx++])) {
    goto STTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST;
  }
  exit(EXIT_SUCCESS);

STTSSSSTSTTSSTSSSTTSSTSS:
  arith_add();
  longjmp(call_stack[--call_stack_idx], 1);

STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST:
  dup_n(0);
  heap_read();
  dup_n(0);
  if (!pop()) {
    goto STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS;
  }
  putchar(pop());
  push(1);
  arith_add();
  goto STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST;

STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS:
  pop();
  pop();
  longjmp(call_stack[--call_stack_idx], 1);

STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS:
  dup_n(0);
  dup_n(0);
  heap[pop()] = getchar();
  heap_read();
  dup_n(0);
  push(10);
  arith_sub();
  if (!pop()) {
    goto STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS;
  }
  pop();
  push(1);
  arith_add();
  goto STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS;

STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS:
  pop();
  push(1);
  arith_add();
  push(0);
  heap_store();
  longjmp(call_stack[--call_stack_idx], 1);

STTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST:
  push(10);
  push(13);
  putchar(pop());
  putchar(pop());
  longjmp(call_stack[--call_stack_idx], 1);

  return EXIT_SUCCESS;
}


inline static int pop(void)
{
  assert(stack_idx < LENGTHOF(stack));
  return stack[--stack_idx];
}


inline static void push(int e)
{
  assert(stack_idx < LENGTHOF(stack));
  stack[stack_idx++] = e;
}


inline static void dup_n(size_t n)
{
  assert(n < stack_idx && stack_idx < LENGTHOF(stack) - 1);
  stack[stack_idx] = stack[stack_idx - (n + 1)];
  stack_idx++;
}


inline static void slide(size_t n)
{
  assert(stack_idx > n);
  stack[stack_idx - (n + 1)] = stack[stack_idx - 1];
  stack_idx -= n;
}


inline static void swap(void)
{
  assert(stack_idx > 1);
  SWAP(int, &stack[stack_idx - 1], &stack[stack_idx - 2]);
}


inline static void arith_add(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  stack[stack_idx - 1] += stack[stack_idx];
}


inline static void arith_sub(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  stack[stack_idx - 1] -= stack[stack_idx];
}


inline static void arith_mul(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  stack[stack_idx - 1] *= stack[stack_idx];
}


inline static void arith_div(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  assert(stack[stack_idx] != 0);
  stack[stack_idx - 1] /= stack[stack_idx];
}


inline static void arith_mod(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  assert(stack[stack_idx] != 0);
  stack[stack_idx - 1] %= stack[stack_idx];
}


inline static void heap_store(void)
{
  int value = pop();
  int addr  = pop();
  assert(0 <= addr && addr < (int) LENGTHOF(heap));
  heap[addr] = value;
}


inline static void heap_read(void)
{
  int addr = pop();
  assert(0 <= addr && addr < (int) LENGTHOF(heap));
  push(heap[addr]);
}
$ ./whitespace.out hworld.ws -t -o hworld.c
$ gcc -Ofast -march=native -DNDEBUG hworld.c -o hworld.out
$ ./hworld.out
Hello, world of spaces!

Wandboxでの実行結果はこちら

参考

C言語でメモリ上のコードを実行する

(あまり低レイヤに詳しくない人間がこの記事を書いているので,信憑性については注意すること)

C言語といえば,自由度の高いプログラミングである. メモリ上に機械語を書いて,それを実行したいという欲求は多くあるだろう. 例えば,次のHello Worldプログラムなんかがそうだ.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>

static int stack[128 * 1024];
static unsigned char code[] = {
  0x53, 0x55, 0x41, 0x54, 0x48, 0x89, 0xfb, 0x48, 0x89, 0xf5, 0x49, 0x89, 0xd4, 0x41, 0x83, 0x04,
  0x24, 0x09, 0x41, 0x8b, 0x04, 0x24, 0x85, 0xc0, 0x0f, 0x84, 0x25, 0x00, 0x00, 0x00, 0x49, 0x83,
  0xc4, 0x04, 0x41, 0x83, 0x04, 0x24, 0x08, 0x49, 0x83, 0xc4, 0x04, 0x41, 0x83, 0x04, 0x24, 0x0b,
  0x49, 0x83, 0xc4, 0x04, 0x41, 0x83, 0x04, 0x24, 0x05, 0x49, 0x83, 0xec, 0x0c, 0x41, 0xff, 0x0c,
  0x24, 0xeb, 0xcf, 0x49, 0x83, 0xc4, 0x04, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x49, 0x83, 0xc4,
  0x04, 0x41, 0x83, 0x04, 0x24, 0x02, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x04, 0x24,
  0x07, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x04,
  0x24, 0x03, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x49, 0x83, 0xc4, 0x04, 0x41, 0xff, 0x0c, 0x24,
  0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x2c, 0x24, 0x0c, 0x49, 0x8b, 0x3c, 0x24, 0xff,
  0xd3, 0x49, 0x83, 0xec, 0x04, 0x41, 0x83, 0x04, 0x24, 0x08, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3,
  0x41, 0x83, 0x2c, 0x24, 0x08, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x04, 0x24, 0x03,
  0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x2c, 0x24, 0x06, 0x49, 0x8b, 0x3c, 0x24, 0xff,
  0xd3, 0x41, 0x83, 0x2c, 0x24, 0x08, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x49, 0x83, 0xc4, 0x04,
  0x41, 0xff, 0x04, 0x24, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x5c, 0x5d, 0x5b, 0xc3,
};

int
main(void)
{
  long page_size = sysconf(_SC_PAGESIZE) - 1;
  mprotect((void *) code, (sizeof(code) + page_size) & ~page_size, PROT_READ | PROT_EXEC);
  ((void (*)(int (*)(int), int (*)(), int *)) (unsigned char *) code)(putchar, getchar, stack);
  return EXIT_SUCCESS;
}

これは64bit上のLinuxならば,おそらく実行可能なコードであろう. コードの元ネタは,herumi/xbyakのサンプルコードのBrainfuck処理系:sample/bf.cppが,Hello Worldを出力するBrainfuckコード

+++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.>-.
------------.<++++++++.--------.+++.------.--------.>+.

を元に出力したC言語ソースコードである.

配列 code[]機械語であり,システムコールmprotect() を用いてこの領域に実行可能属性を付加している. 実行可能にしたメモリ領域の先頭を指すポインタを取得し,それを関数ポインタとしてコールすると,その領域の機械語を実行できるというカラクリのようだ.

mprotect()mmap() で確保した領域にのみにしか用いることができないが,staticなグローバル変数ならば問題はないらしい(このあたりについてのことは,僕の力量不足で調査できていない). また,僕の環境では,malloc() で確保した領域に mprotect() を用いたが,うまくいかなかった.

ところで,システムコールmprotect() は当然ながらWindows環境下で用いることはできない. Windowsで,ある領域に実行可能属性を付加するにはどうすればいいのだろうか? 調査したところ,Windows APIVirtualProtectmprotect() に相当するらしい. 前述のC言語ソースコードWindows(64bit)用に書き直すと,以下のようになる. もちろん,機械語部分はWindows用に書き直してある.

#include <stdio.h>
#include <stdlib.h>
#ifndef WIN32_LEAN_AND_MEAN
#  define WIN32_LEAN_AND_MEAN
#  define WIN32_LEAN_AND_MEAN_IS_NOT_DEFINED
#endif
#include <windows.h>
#ifdef WIN32_LEAN_AND_MEAN_IS_NOT_DEFINED
#  undef WIN32_LEAN_AND_MEAN_IS_NOT_DEFINED
#  undef WIN32_LEAN_AND_MEAN
#endif

static int stack[128 * 1024];
static unsigned char code[] = {
  0x56, 0x57, 0x55, 0x48, 0x89, 0xce, 0x48, 0x89, 0xd7, 0x4c, 0x89, 0xc5, 0x83, 0x45, 0x00, 0x09,
  0x8b, 0x45, 0x00, 0x85, 0xc0, 0x0f, 0x84, 0x21, 0x00, 0x00, 0x00, 0x48, 0x83, 0xc5, 0x04, 0x83,
  0x45, 0x00, 0x08, 0x48, 0x83, 0xc5, 0x04, 0x83, 0x45, 0x00, 0x0b, 0x48, 0x83, 0xc5, 0x04, 0x83,
  0x45, 0x00, 0x05, 0x48, 0x83, 0xed, 0x0c, 0xff, 0x4d, 0x00, 0xeb, 0xd4, 0x48, 0x83, 0xc5, 0x04,
  0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x83,
  0xc5, 0x04, 0x83, 0x45, 0x00, 0x02, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6,
  0x48, 0x83, 0xc4, 0x20, 0x83, 0x45, 0x00, 0x07, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20,
  0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6,
  0x48, 0x83, 0xc4, 0x20, 0x83, 0x45, 0x00, 0x03, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20,
  0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x83, 0xc5, 0x04, 0xff, 0x4d, 0x00, 0x48, 0x8b, 0x4d,
  0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x83, 0x6d, 0x00, 0x0c, 0x48,
  0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x83, 0xed,
  0x04, 0x83, 0x45, 0x00, 0x08, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48,
  0x83, 0xc4, 0x20, 0x83, 0x6d, 0x00, 0x08, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff,
  0xd6, 0x48, 0x83, 0xc4, 0x20, 0x83, 0x45, 0x00, 0x03, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec,
  0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x83, 0x6d, 0x00, 0x06, 0x48, 0x8b, 0x4d, 0x00, 0x48,
  0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x83, 0x6d, 0x00, 0x08, 0x48, 0x8b, 0x4d,
  0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x83, 0xc5, 0x04, 0xff,
  0x45, 0x00, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20,
  0x5d, 0x5f, 0x5e, 0xc3,
};

int
main(void)
{
  DWORD old_protect;
  VirtualProtect((LPVOID) code, sizeof(code), PAGE_EXECUTE_READWRITE, &old_protect);
  ((void (*)(int (*)(int), int (*)(), int *)) (unsigned char *) code)(putchar, getchar, stack);
  return EXIT_SUCCESS;
}

ちなみに,Cygwinだと mprotect() を利用可能であるが,使用しても意味がなかった. Cygwinでも VirtualProtect() を用いなければならない.

まとめ

mprotect() または VirtualProtect() を用いることによって,メモリのパーミッションを変更できる. 実行可能属性を付加することにより,動的に生成した機械語を実行することも可能である. これがJITコンパイルの基礎となる仕組みなのだろう.

NeoBundleの使い方

他人の.vimrcを見ていると,NeoBundleを用いてプラグインの管理をしている人が多い. かくいう僕もその一人である. しかし,NeoBundleの事情に詳しくない人は,どうも古い情報を参照しているため,以下のような古い仕様のNeoBundleに沿った記述や,そもそもイケてない記述をしていることがある.

if has('vim_starting')
  " ~/.vimrcの読み込みならば,自動的にnocompatibleになっているはず
  set nocompatible
  " 最近のNeoBundleでは必要ない
  filetype off
  set runtimepath+=~/.vim/bundle/neobundle.vim
endif
" neobundle#rc() より,neobundle#begin() を用いるべき
call neobundle#rc(expand('~/.vim/bundle/'))
" neobunde.vim自体は読み込みを行わないNeoBundleFetchで管理すべき
NeoBundle 'Shougo/neobundle.vim'

NeoBundle 'foo/bar'
NeoBundleLazy 'hoge/piyo', {
      \ 'autoload': {
      \   'commands': ['Piyo'],
      \ }
      \}

" ...

filetype plugin indent on

set nocompatibleは副作用の多いオプション設定で,他のオプション設定も変更することになる. 例えば,オプションhistoryの値がデフォルトの50に書き換えられたりする. デフォルトでvimが使用する.vimrc,例えば,~/.vimrcであれば,自動的にset nocompatibleになっているので,わざわざ.vimrc中で設定することは無い. また,

$ vim -u ~/other.vimrc

等としてvimを起動する場合であっても,オプション-Nを付加して起動すればよい. どうしても,set nocompatibleと書いて安心したいのであれば,

if !&compatible
  set nocompatible
endif

のように,互換モードか否かを判断してからにするべきだろう.

filetype offは,最近のNeoBundleでは内部で行っているため,わざわざ.vimrcに記述する必要が無いそうだ.

neobundle#rc()を用いていた場合,警告が出るので,言わずともneobundle#begin()neobundle#end()を用いる設定に書き換えることだろう.

以上のことを踏まえると,最近のNeoBundleでは以下のように記述するのが良い.

if has('vim_starting')
  set rtp+=~/.vim/bundle/neobundle.vim
endif
call neobundle#begin()
NeoBundleFetch 'Shougo/neobundle.vim'

NeoBundle 'foo/bar'
NeoBundleLazy 'hoge/piyo', {
      \ 'autoload': {
      \   'commands': ['Piyo'],
      \ }
      \}
call neobundle#end()
filetype plugin indent on

if !has('vim_starting')
  call neobundle#call_hook('on_source')
endif

また,NeoBundleのキャッシュ機能を利用するならば,以下のように記述する. (キャッシュ機能は実装当初と現在を比較すると,大きく仕様変更されているが,古い方の仕様は紹介しない)

if has('vim_starting')
  set rtp+=~/.vim/bundle/neobundle.vim
endif
call neobundle#begin()

" neobundle#load_cache() はキャッシュが最新ならば,0を返却する関数
if neobundle#load_cache()
  NeoBundleFetch 'Shougo/neobundle.vim'
  NeoBundle 'foo/bar'
  " Lazyの設定等はキャッシュされる
  NeoBundleLazy 'hoge/piyo', {
        \ 'autoload': {
        \   'commands': ['Piyo'],
        \ }
        \}
  NeoBundleSaveCache
endif
call neobundle#end()
filetype plugin indent on

" キャッシュされないプラグインのグローバル変数等の設定をここに
" neobundle#tap() は引数のプラグインがロードされているならば,1を返却する
if neobundle#tap('bar')
  let g:bar#var01 = 10
  let g:bar#var02 = ['a', 'b', 'c']
  call neobundle#untap()
endif

if neobundle#tap('piyo')
  " hooks.on_source() でプラグイン読み込み時の処理を指定できる
  " この程度(グローバル変数を設定するだけ)の処理ならば,無理に利用する必要はない
  " 時間のかかる処理などを記述すると吉
  function! neobundle#tapped.hooks.on_source(bundle) abort
    let g:piyo#var01 = 1
    let g:piyo#var02 = 'abc'
  endfunction
  call neobundle#untap()
endif

if !has('vim_starting')
  call neobundle#call_hook('on_source')
endif

memolist.vimの遅延読み込み設定

glidenote/memolist.vimというメモを取るためのVimプラグインがある, Shougo/neobundle.vimでこのプラグインを管理し,遅延読み込み設定を行った.

NeoBundleLazy 'glidenote/memolist.vim', {
      \ 'autoload': {'commands': ['MemoGrep', 'MemoList', 'MemoNew']}
      \}

しかし遅延読み込み設定をした場合,デフォルト設定のままだと :MemoList を実行しても,1回目はメモファイル一覧が表示できなかった. すなわち,Netrwでメモを保存したディレクトリを開くことができなかった. (僕の環境でのみ起こる問題かもしれない)

そこで,以下のようにコマンドを再定義し,処理を2回呼び出すようにして,この問題を回避した. (functionsfunction_prefix を指定していないが,うまくいった)

NeoBundleLazy 'glidenote/memolist.vim', {
      \ 'autoload': {'commands': ['MemoGrep', 'MemoNew']}
      \}
command! -nargs=0 MemoList  silent call memolist#list() | call memolist#list()

:MemoListShougo/unite.vimShougo/vimfiler.vimを用いるようにした場合,このような問題は発生しなかった.