koturnの日記

転職したい社会人1年生の技術系日記

IQ1を支えるコーディング術

この記事はIQが1Advent Calendarの10日目の記事になります. 昨日はMew_1406さんのIQ1と謝罪行脚と題された,怖いお話でしたね.

はじめに

ご存知の通り,僕はIQ1です. IQ1には様々な困難が存在します.

例えば,物が覚えられない.... 僕はプログラムを書くことがあるのですが,基本的なイディオムや標準ライブラリの関数の使い方等を覚えられず,このために苦戦することがあります.

そこで,この記事ではIQ1でも多少楽にプログラムを書く手法を紹介したいと思います. 特に,僕が書くことのあるC++に焦点を当てたいと思います.

スニペット展開

基本的にほとんどのエディタには「スニペット展開」という機能が標準,あるいはプラグインという形で利用可能です. スニペット展開とは何か,それは以下のGIF画像を見てもらうのが早いでしょう.

f:id:koturn:20171210023943g:plain

このように,事前に登録しておいたコードのテンプレートを挿入するのが,コードスニペットの展開というものになります. これは単なるコーディング速度の向上だけでなく,僕のようなIQ1にとっては記憶補助にもなるわけです.

前提

僕は普段テキストエディタとしてVimを使っています. ですので,この記事において,特に断りがない限り,Vimを使っていることを前提とします. また,Vimスニペット展開を行うプラグインとして,Shougo/neosnippet.vimを使うこととします

ただ,スニペット展開自体は,前述の通り,今頃のテキストエディタにもあるものなので,Vimはあくまで一例としてください.

neosnippetの導入

Vimneosnippet.vimを前提とするので,最小限のインストール方法について記載します.

プラグインマネージャに,Shougo/dein.vimを使っているなら,.vimrcに以下のような記述を加えると,使用できるようになるはずです. neosnippet.vimは補完プラグインとして,Shougo/deoplete.nvimShougo/neocomplete.vimを導入しておくと,ベンリさが100倍になるので,一緒に入れておきましょう. Neovim,Vim8(Windows除く)の場合はShougo/deoplete.nvim,Vim7やWindowsの場合はneocomplete.vimを導入する設定になっています. (Windowsではdeopleteの補完が遅いと聞いたので除外)

set encoding=utf-8

if has('vim_starting')
  let s:deindir = expand('~/.cache/dein')
  let s:deinlocal = s:deindir . '/repos/github.com/Shougo/dein.vim'
  let &runtimepath = s:deinlocal . ',' . &runtimepath
endif

if dein#load_state(s:deindir)
  call dein#begin(s:deindir)
  call dein#add('Shougo/dein.vim')
  if has('nvim') || !has('win32') && v:version >= 704
    call dein#add('Shougo/deoplete.nvim', {
        \ 'on_event': 'InsertEnter',
        \})
    if !has('nvim')
      call dein#add('roxma/nvim-yarp')
      call dein#add('roxma/vim-hug-neovim-rpc')
    endif
  elseif v:version > 703 || (v:version == 703 && has('patch885'))
    if has('lua')
      call dein#add('Shougo/neocomplete.vim', {
          \ 'on_event': 'InsertEnter',
          \ 'on_cmd': [
          \   'NeoCompleteEnable',
          \   'NeoCompleteDisable',
          \   'NeoCompleteLock',
          \   'NeoCompleteUnlock',
          \   'NeoCompleteToggle',
          \   'NeoCompleteSetFileType',
          \   'NeoCompleteClean',
          \   'NeoCompleteBufferMakeCache',
          \   'NeoCompleteDictionaryMakeCache',
          \   'NeoCompleteSyntaxMakeCache',
          \   'NeoCompleteTagMakeCache'
          \ ]
          \})
    else
      call dein#add('Shougo/neocomplcache', {
            \ 'on_event': 'InsertEnter',
            \ 'on_cmd': [
            \   'NeoComplCacheEnable',
            \   'NeoComplCacheDisable',
            \   'NeoComplCacheLock',
            \   'NeoComplCacheUnlock',
            \   'NeoComplCacheToggle',
            \   'NeoComplCacheLockSource',
            \   'NeoComplCacheUnlockSource',
            \   (v:version >= 703 ? 'NeoComplCacheSetFileType' : 'NeoComplCacheSetFileType'),
            \   'NeoComplCacheSetFileType',
            \   'NeoComplCacheClean',
            \ ],
            \ 'on_map': [['is', '<Plug>(neocomplcache_snippets_']]
            \})
    endif
  endif
  call dein#add('Shougo/neosnippet', {
        \ 'on_event': 'InsertEnter',
        \ 'on_cmd': [
        \   'NeoSnippetEdit',
        \   'NeoSnippetMakeCache',
        \   'NeoSnippetSource',
        \   'NeoSnippetClearMarkers'
        \ ],
        \ 'on_ft': 'neosnippet',
        \ 'on_map': [['nisx', '<Plug>(neosnippet_']],
        \})
  call dein#add('Shougo/neosnippet-snippets')
  call dein#end()
  call dein#save_state()
endif

if dein#tap('deoplete.nvim')
  let g:deoplete#enable_at_startup = 1
endif

if dein#tap('neocomplete.vim')
  let g:neocomplete#enable_at_startup = 1
endif

if dein#tap('neocomplcache')
  let g:neocomplcache_enable_at_startup = 1
endif

if dein#tap('neosnippet')
  imap <C-k>  <Plug>(neosnippet_expand_or_jump)
  smap <C-k>  <Plug>(neosnippet_expand_or_jump)
  imap <expr><TAB>  neosnippet#expandable() <Bar><Bar> neosnippet#jumpable() ?
        \ "\<Plug>(neosnippet_expand_or_jump)" : pumvisible() ? "\<C-n>" : "\<TAB>"
  smap <expr><TAB>  neosnippet#expandable() <Bar><Bar> neosnippet#jumpable() ?
        \ "\<Plug>(neosnippet_expand_or_jump)" : "\<TAB>"
  let g:neosnippet#snippets_directory = '~/.vim/neosnippets'
  let g:neosnippet#expand_word_boundary = 1
endif

filetype plugin indent on
syntax enable

動作確認

まず,

$ vim main.cpp

として,Vimを起動し,インサートモードに入り,main<C-k>としてみましょう. 以下のgifアニメのようになれば問題ありません.

f:id:koturn:20171210023827g:plain

なお,このgifアニメでは,前述の最低限の .vimrc を用いたときのスクリーンキャストを貼っていますが,これ以降は僕が普段使用している .vimrc でのスクリーンキャストを貼り付けます.

自分でスニペットを定義する

さて,ここまでで,Vimスニペットプラグインを導入し,デフォルトのスニペットを展開することをしました. しかし,スニペットとは自分で追加していくもの...ここでは僕が実際に普段使っているC++スニペットを紹介することにしましょう.

neosnippetのファイル配置

今回は ~/.vim/neosnippets/スニペットファイルを置きます. 前述の設定例でも,このパスを指定していますね.

C++スニペットの場合は, cpp.snip というファイル名にしなければなりません. (つまり,フルパスは ~/.vim/neosnippets/cpp.snip

neosnippetのスニペット定義について

:h neosnippet-snippet-syntax を見るのが早いですが,簡単に.

snippet [[スニペットのトリガー]]
abbr [[deoplet等の補完時に出てくる説明 (省略可)]]
alias [[別のトリガーを指定可能 (省略可)]]
regex [[ここに記述した正規表現にマッチしている場合にのみ展開可能 (省略可)]]
option [[説明が面倒なので,ヘルプを見て]]
  [[1段階インデントした位置にスニペット本体を記述]]

つまり,最低限のスニペットの例は以下のようになります.

snippet rbf
  for (auto&& ${1:e} : ${2:#:container}) {
    ${0}
  }

さて,よくわからない記述が出てきましたね. ${数字}<C-k> を押下する度にカーソルの移動する位置を表していて,そのときのデフォルトの展開結果を指定してたりします. ${0}は最終的にカーソルが移動する位置, ${1:e}e をデフォルトの展開結果(デフォルトで e が挿入される)とした,1番目のカーソルの移動位置, ${2:#:container} は2番目のカーソル移動位置で, container と表示はするものの,${1:e} と違って実際にテキスト挿入を行わないもの(コメント的なもの)となっています. 単に ${1} と書いた場合,デフォルト値が無いカーソル移動位置となります.

まぁ,詳細は :h neosnippet-snippet-syntax を見るなり,Shougo/neosnippet-snippetsの例を見てください.

main()関数

まずは main() からだよね,ということで,スニペットを作ってみます. まぁ,これはデフォルトのスニペットでも定義されているのですが,細かい部分で好みに合わないので,書き換えます.

C++のmain関数は

int
main(int argc, const char* argv[])
{

  return EXIT_SUCCESS;
}

って感じで書くので,単純に以下のようにスニペット化します. デフォルトで定義されているのを無効化するために,直前にdelete mainを書きます.

snippet main
  int
  main(${1:int argc, const char* argv[]})
  {
    ${0};
    return EXIT_SUCCESS;
  }

すると,素早く以下のようにmain関数を書けるようになります.

f:id:koturn:20171210023837g:plain

競プロをやっている人は普段使ってるテンプレートをスニペット登録しておくと便利かもしれないですね.

インクルードガード

ヘッダファイルにはインルードガードを書きますよね. コンパイラ拡張をなるべく嫌う派としては, #pragma を使わず,

#ifndef HOGE_H
#define HOGE_H

#endif

と書くと思いますが...面倒! 特に HOGE_H が2回登場しているので,これはスニペット化を考えるところです. そこで以下のスニペットを用意しましょう. (僕は #endif の後ろにコメント入れる主義なので,それも加えています)

snippet inc_guard
  #ifndef ${1:#:NAME}
  #define $1

  {0}

  #endif  // $1

すると, inc_guard<C-k> の入力で以下のように素早くインクルードガードを記述できます.

f:id:koturn:20171210023841g:plain

std::arraystd::vector 等の合計値を出す

std::arraystd::vector の要素の合計値を出したいときに困ることのひとつに std::sum() みたいな単純な関数が無いことがあると思います. <numeric>std::accumulate() を使えばいいのですが,

// #include <algorithm>
// #include <vector>
// #include <numeric>
// #include <random>

// 以下の2行は乱数入れてるだけなので,無視していいです
std::vector<int> vct(10);
std::generate(std::begin(vct), std::end(vct), std::mt19937(std::random_device{}()));
// 合計値を出すだけなのに,タイプ数が多い
auto sum = std::accumulate(std::cbegin(vct), std::cend(vct), 0);

このように,非常にタイプ数が多くて困りものです. なので,IQ1の僕は無い知恵を絞り,以下のスニペットを登録しました.

snippet sum
  std::accumulate(std::cbegin(${1}), std::cend($1), ${2:decltype($1)::value_type()})

第三引数を decltype(vct)::value_type() のように展開できるようにしておくと,要素型の値が何であってもデフォルト値を利用できるようになるので便利です. 例えば,int 型なら 0 ですし, double 型なら 0.0 ですね. (要素型が double であるのに,第三引数に 0 を指定すると,恐らく望んだ結果は返ってこないので,そういう事故防止にも役立ちます)

f:id:koturn:20171210023943g:plain

C++11以降の <regex>正規表現マッチングによるループ

C++11になり <regex> が追加されて,C++でも気軽に正規表現が利用できる時代となりました. しかし,ここで問題が1つあります. それは,<regex> は利用に際し,やや複雑なコードが要求されることです.

例えば,正規表現のマッチングによるループの例を見てみましょう.

// #incluse <iostream>
// #incluse <regex>
// #incluse <string>

std::string text("2017-12-10 12:34:56");
std::regex ptn("\\d+");
for (std::sregex_iterator itr = std::sregex_iterator(std::cbegin(text), std::cend(text), ptn), end; itr != end; ++itr) {
  std::cout << itr->str() << std::endl;
}

長い...こんなもの,とてもIQ1が気軽に利用できるものではありません!

しかし,こういうものはテンプレ,そして共通項があるというもの.

エイヤッと以下のスニペットを用意しましょう. 入力すべき項目は以下の4つです.

  1. イテレータの変数名
  2. 対象となる std::string の変数名
  3. マッチパターンとなる std::regex の変数名
  4. イテレータの終端を表す std::sregex_iterator のデフォルトでコンストラクトされたものを格納する変数名

4つ目は正直,決め打ちでもよいかな感はあるのですが,一応指定できるようにしておくと,同じブロックで既に変数名が使用されていた場合に対応できるというメリットがあります.

snippet regex_match_loop
  for (std::sregex_iterator ${1:itr} = std::sregex_iterator(std::cbegin(${2:#:text}), std::cend($2), ${3:#:regex}), ${4:end}; $1 != $4; ++$1) {
    ${0}
  }

f:id:koturn:20171210023947g:plain

スリープ

かつて,C++では,標準ライブラリでスリープを行う関数が提供されておらず,各環境に応じたスリープ系の関数を呼び出す必要がありました. 時は移り,C++11... <chrono><thread> が追加され,標準ライブラリの関数でスリープを行うことが可能になりました. ただし,やや長ったらしい記述が必要となるため,IQ1にとっては書くのが苦痛です.

// #include <chrono>
// #include <thread>
std::this_thread::sleep_for(std::chrono::milliseconds(1000));

スリープに必要なのは以下の2つ.

  1. 時間分解能(ミリ秒とか)
  2. どの程度スリープするか

なので,以下のスニペットを用意してみましょう.

std::this_thread::sleep_for(std::chrono::${1:milliseconds}(${2:1000}));

f:id:koturn:20171210023950g:plain

<algorithm> の関数にラムダを渡す

最もスニペットが有用なのは,ラムダ取る <algorithm> の関数のスニペットでしょう. 例えば, std::array の要素の内,3で割り切れるものがいくつあるか数えるコードを考えると,

// #include <algorithm>
// #include <array>
// #include <numeric>
// #include <iostream>

// 要素数とか入れる値は適当で
std::array<int, 10> arr;
std::iota(std::begin(arr), std::end(arr), 0);

auto cnt = std::count_if(
  std::cbegin(arr),
  std::cend(arr),
  [](const auto& e) {
    e % 3 == 0;
  });

と書くと思います. しかし,これはかなり面倒. 特にラムダの辺りが嫌な感じですね.

これをスニペットにすると以下のような感じでしょうか.

snippet count_if
alias count_f
abbr std::count_if <algorithm>
  std::count_if(
    std::cbegin(${1}),
    std::cend($1),
    [](const auto& ${2:e}) {
      return ${0};
    });

f:id:koturn:20171210023954g:plain

記述量が減って,かなり快適に書けるようになりました! ここで紹介している std:count_if だけでなく, std::sortstd::accumulate のラムダを取る版のスニペットを用意しておくと,非常に便利になるでしょう.

スニペットファイルを用意しました!!!

さて,ここまで紹介してきたneosnippetのC++用のスニペット定義ファイルを用意しましたC++11, C++14, C++17用と用意しています. 適当にコピペするなり,改変するなりして使ってください.

差異

C++11/C++14/C++17用のスニペットの差異は以下の通りです. 普段利用しているコンパイラに応じたものを使うといいでしょう. 例えば,競プロのジャッジサーバにC++11のコンパイラしか入っていないのであれば,C++11を使うのがよいでしょう.

C++11 -> C++14

std::sort()の述語やstd::accumulate()の集計関数等のラムダにジェネリックラムダを利用するように

decltype(vct)::value_type は見た目的に長いので,短い記法を使ってスッキリさせようというやつです. 関数等で参照として受け取った std::vector 等に対して用いる場合でも,いちいち std::remove_reference を狭まなくてよくなるので,楽ですね.

# before
std::sort(
  std::begin(${1}),
  std::end($1),
  [](const decltype($1)::value_type& x, const decltype($1)::value_type& y) {
    return ${0:x < y};
  });

# after
std::sort(
  std::begin(${1}),
  std::end($1),
  [](const auto& x, const auto& y) {
    return ${0:x < y};
  });

<algorithm> の読取専用のイテレータ引数に対して,std::begin()/std::end()でなく.std::cbegin()/std::cend()を用いるように

生成されるコードは変わらないと思うんですが,const付けられるものに対してはconstの方がいいよねというやつです. フリー関数の std::begin(), std::end()C++11で追加されたんですが,何故か std::cbegin(), std::cend()C++14になってから追加されたので,それに合わせた変更になります. C++11でもメンバー関数版の cbegin(), cend() 使えばいいじゃないという話になりそうですが, std::begin(), std::end() と釣り合いが取れなくなって気持ち悪いので....

# before (C++11)
snippet sum
  std::accumulate(std::begin(${1}), std::end($1), ${2:decltype($1)::value_type()})

# after (C++14)
snippet sum
  std::accumulate(std::cbegin(${1}), std::cend($1), ${2:decltype($1)::value_type()})

<type_traits> の関数

C++11では型の取得のために ::type にアクセスしていましたが,より簡潔に書けるようになったので,そちらを利用.

# before
snippet decay
  std::decay<T>::type

# after
snippet decay
  std::decay_t<T>

C++14 -> C++17

<type_traits> の関数

C++14までは型の判定に使用できるメタ関数の真偽値は value メンバーから取得していましたが,C++17ではもっと楽に取得できるようになったので,そちらを用いるようにしました.

まとめ

なお,neosnippet.vimの後続として,deoppet.nvimが開発されているとのことです. Vimconf 2017でShougoさんは,スニペットファイルはneosnippetと同じ形式とおっしゃていたと思うので,今の内にneosnippet用のスニペットファイルを充実させても損にはならないと思います.

最後に

ちゃっくさんの金で肉が食べたい!

明日は,shrcyanさんの記事になります.楽しみですね.

小さいHello Worldバイナリを作る

はじめに

先日は,少し不思議なHello Worldを紹介した. そこで思ったのが,この程度の小さいプログラムならば,gccは必要ないのではないかと思い至った. そこで,小さいHello Worldの実行ファイルを作ることにした.

方針

終了の仕方

先日の記事では,crt*.o ありきであったため,プログラマ視点でのプログマムのエントリポイントが main であったため,単に return 0; すればよかった. しかし,今回は crt*.o とのリンクは行わないため, return 0; に代わり, exit システムコールを呼び出す必要がある.

文字列データのアドレス

実行ファイルを生成するならば,"Hello World!\n" のアドレスも既知となる. したがって,ripから現在の実行中のアドレスを取得する必要はない.

また,前回と同様,文字列データは,コードの末尾に置くものとする.

セクションヘッダを削る

通常の実行ファイルは,

  1. ELFヘッダ
  2. プログラムヘッダ(複数)
  3. コードデータ
  4. セクション名テーブル
  5. セクションヘッダ(複数)

の5つを含むが 4.と5.は無くても良いので,以下の3つで実行ファイルを構成する.

  1. ELFヘッダ
  2. プログラムヘッダ(複数)
  3. コードデータ

セクションヘッダを削るため,$ objdump -d が効かなくなるが,まぁ良しとしよう.

実行ファイルの生成

上記の方針を踏まえ,実行ファイルを作るプログラムを書く. C言語で書く必要は無いのだが,ELFヘッダ,プログラムヘッダの構造体を利用することが可能なため,意外とCで書くのが楽になる. ただし,前回と同様,x64向けのプログラムを作る.

生成する実行ファイル名は a.out で固定しており,また,生成後に実行権限を付与し,実行するようにしてある.

これで,サイズにして171 bytesのHello Worldプログラムを作成できる.

コードの説明は特にしないが,構造体のメンバを見れば,どこで指定しているかは容易にわかる.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <elf.h>

#define BASE_ADDR    0x08000000
#define N_PROGRAM_HEADER  1
#define N_SECTION_HEADER  0
#define HEADER_SIZE  (sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) * N_PROGRAM_HEADER)


int
main(void)
{
  unsigned int str_addr = BASE_ADDR + HEADER_SIZE + 37;
  Elf64_Ehdr ehdr;
  Elf64_Phdr phdr;
  char code[] =
    // mov $0x01,%rax  # System call number (write)
    "\x48\xc7\xc0\x01\x00\x00\x00"
    // mov $0x0d,%edx  # Third argument
    "\xba\x0d\x00\x00\x00"
    // mov $0x********,%rsi  # Second argument
    "\x48\xc7\xc6\x00\x00\x00\x00"
    // mov $0x01,%edi  # First argument
    "\xbf\x01\x00\x00\x00"
    // syscall
    "\x0f\x05"
    // mov $0x3c,%rax  # System call number (exit)
    "\x48\xc7\xc0\x3c\x00\x00\x00"
    // xor %edi,%edi   # First argument
    "\x31\xff"
    // syscall
    "\x0f\x05"
    // String data
    "Hello World!\n";
  FILE *f = fopen("a.out", "wb");
  if (f == NULL) {
    fputs("Failed to open a.out\n", stderr);
    return EXIT_FAILURE;
  }

  // ELF header
  ehdr.e_ident[EI_MAG0] = ELFMAG0;
  ehdr.e_ident[EI_MAG1] = ELFMAG1;
  ehdr.e_ident[EI_MAG2] = ELFMAG2;
  ehdr.e_ident[EI_MAG3] = ELFMAG3;
  ehdr.e_ident[EI_CLASS] = ELFCLASS64;
  ehdr.e_ident[EI_DATA] = ELFDATA2LSB;
  ehdr.e_ident[EI_VERSION] = EV_CURRENT;
  ehdr.e_ident[EI_OSABI] = ELFOSABI_LINUX;
  ehdr.e_ident[EI_ABIVERSION] = 0x00;
  ehdr.e_ident[EI_PAD] = 0x00;
  ehdr.e_type = ET_EXEC;
  ehdr.e_machine = EM_X86_64;
  ehdr.e_version = EV_CURRENT;
  ehdr.e_entry = BASE_ADDR + HEADER_SIZE;
  ehdr.e_phoff = sizeof(Elf64_Ehdr);
  ehdr.e_shoff = 0;
  ehdr.e_flags = 0x00000000;
  ehdr.e_ehsize = sizeof(Elf64_Ehdr);
  ehdr.e_phentsize = sizeof(Elf64_Phdr);
  ehdr.e_phnum = N_PROGRAM_HEADER;
  ehdr.e_shentsize = 0;
  ehdr.e_shnum = N_SECTION_HEADER;
  ehdr.e_shstrndx = SHN_UNDEF;
  fwrite(&ehdr, sizeof(ehdr), 1, f);

  // Program header
  phdr.p_type = PT_LOAD;
  phdr.p_flags = PF_R | PF_X;
  phdr.p_offset = 0x000000000000000000;
  phdr.p_vaddr = BASE_ADDR;
  phdr.p_paddr = BASE_ADDR;
  phdr.p_filesz = HEADER_SIZE + sizeof(code);
  phdr.p_memsz = HEADER_SIZE + sizeof(code);
  phdr.p_align = 0x0000000000000100;
  fwrite(&phdr, sizeof(phdr), 1, f);

  // Put string data address into code
  memcpy(code + 15, &str_addr, sizeof(str_addr));
  // Write .text section
  fwrite(code, 1, sizeof(code), f);

  fclose(f);
  f = NULL;

  // Show file size
  printf("Size of a.out: %lu bytes\n", HEADER_SIZE + sizeof(code));
  fflush(stdout);

  // Give execution permission
  chmod("a.out", 0755);
  // Execute created binary
  system("./a.out");

  return EXIT_SUCCESS;
}

コンパイルと実行結果は以下の通り.

$ gcc -O2 gen_hello_x64.c -o gen_hello_x64
$ ./gen_hello_x64
Size of a.out: 171 bytes
Hello World!

また,逆コンパイル結果は以下の通り. 前述した通り,セクションヘッダが無いため,プログラムデータ本体を見ることができない.

$ objdump -d a.out

a.out:     file format elf64-x86-64

おまけ: セクションヘッダを付ける

丁寧にセクションヘッダも付けるとしたら,プログラムは以下のようになる.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <elf.h>

#define BASE_ADDR    0x08000000
#define N_PROGRAM_HEADER  1
#define N_SECTION_HEADER  3
#define HEADER_SIZE  (sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) * N_PROGRAM_HEADER)
#define FOOTER_SIZE  (sizeof(Elf64_Shdr) * N_SECTION_HEADER)

const char SH_STR_TBL[] = "\0.text\0.shstrtbl";


int
main(void)
{
  unsigned int str_addr = BASE_ADDR + HEADER_SIZE + 37;
  Elf64_Ehdr ehdr;
  Elf64_Phdr phdr;
  Elf64_Shdr shdr;
  char code[] =
    /* mov $0x01,%rax  # System call number (write) */
    "\x48\xc7\xc0\x01\x00\x00\x00"
    /* mov $0x0d,%edx  # Third argument */
    "\xba\x0d\x00\x00\x00"
    /* mov $0x********,%rsi  # Second argument */
    "\x48\xc7\xc6\x00\x00\x00\x00"
    /* mov $0x01,%edi  # First argument */
    "\xbf\x01\x00\x00\x00"
    /* syscall */
    "\x0f\x05"
    /* mov $0x3c,%rax  # System call number (exit) */
    "\x48\xc7\xc0\x3c\x00\x00\x00"
    /* xor %edi,%edi  # First argument */
    "\x31\xff"
    /* syscall */
    "\x0f\x05"
    /* String data */
    "Hello World!\n";
  FILE *f = fopen("a.out", "wb");
  if (f == NULL) {
    fputs("Failed to open a.out\n", stderr);
    return EXIT_FAILURE;
  }

  /* ELF header */
  ehdr.e_ident[EI_MAG0] = ELFMAG0;
  ehdr.e_ident[EI_MAG1] = ELFMAG1;
  ehdr.e_ident[EI_MAG2] = ELFMAG2;
  ehdr.e_ident[EI_MAG3] = ELFMAG3;
  ehdr.e_ident[EI_CLASS] = ELFCLASS64;
  ehdr.e_ident[EI_DATA] = ELFDATA2LSB;
  ehdr.e_ident[EI_VERSION] = EV_CURRENT;
  ehdr.e_ident[EI_OSABI] = ELFOSABI_LINUX;
  ehdr.e_ident[EI_ABIVERSION] = 0x00;
  ehdr.e_ident[EI_PAD] = 0x00;
  ehdr.e_type = ET_EXEC;
  ehdr.e_machine = EM_X86_64;
  ehdr.e_version = EV_CURRENT;
  ehdr.e_entry = BASE_ADDR + HEADER_SIZE;
  ehdr.e_phoff = sizeof(Elf64_Ehdr);
  ehdr.e_shoff = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL);
  ehdr.e_flags = 0x00000000;
  ehdr.e_ehsize = sizeof(Elf64_Ehdr);
  ehdr.e_phentsize = sizeof(Elf64_Phdr);
  ehdr.e_phnum = N_PROGRAM_HEADER;
  ehdr.e_shentsize = sizeof(Elf64_Shdr);
  ehdr.e_shnum = N_SECTION_HEADER;
  ehdr.e_shstrndx = 1;
  fwrite(&ehdr, sizeof(ehdr), 1, f);

  /* Program header */
  phdr.p_type = PT_LOAD;
  phdr.p_flags = PF_R | PF_X;
  phdr.p_offset = 0x000000000000000000;
  phdr.p_vaddr = BASE_ADDR;
  phdr.p_paddr = BASE_ADDR;
  phdr.p_filesz = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE;
  phdr.p_memsz = HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE;
  phdr.p_align = 0x0000000000000100;
  fwrite(&phdr, sizeof(phdr), 1, f);

  /* Put string data address into code */
  memcpy(code + 15, &str_addr, sizeof(str_addr));
  /* Write .text section */
  fwrite(code, 1, sizeof(code), f);

  /* Write section header names */
  fwrite(SH_STR_TBL, 1, sizeof(SH_STR_TBL), f);

  /* First section header */
  shdr.sh_name = 0;
  shdr.sh_type = SHT_NULL;
  shdr.sh_flags = 0x0000000000000000;
  shdr.sh_addr = 0x0000000000000000;
  shdr.sh_offset = 0x0000000000000000;
  shdr.sh_size = 0x0000000000000000;
  shdr.sh_link = 0x00000000;
  shdr.sh_info = 0x00000000;
  shdr.sh_addralign = 0x0000000000000000;
  shdr.sh_entsize = 0x0000000000000000;
  fwrite(&shdr, sizeof(shdr), 1, f);

  /* Second section header (.shstrtbl) */
  shdr.sh_name = 7;
  shdr.sh_type = SHT_STRTAB;
  shdr.sh_flags = 0x0000000000000000;
  shdr.sh_addr = 0x0000000000000000;
  shdr.sh_offset = HEADER_SIZE + sizeof(code);
  shdr.sh_size = sizeof(SH_STR_TBL);
  shdr.sh_link = 0x00000000;
  shdr.sh_info = 0x00000000;
  shdr.sh_addralign = 0x0000000000000001;
  shdr.sh_entsize = 0x0000000000000000;
  fwrite(&shdr, sizeof(shdr), 1, f);

  /* Third section header (.text) */
  shdr.sh_name = 1;
  shdr.sh_type = SHT_PROGBITS;
  shdr.sh_flags = SHF_EXECINSTR | SHF_ALLOC;
  shdr.sh_addr = BASE_ADDR + HEADER_SIZE;
  shdr.sh_offset = HEADER_SIZE;
  shdr.sh_size = sizeof(code);
  shdr.sh_link = 0x00000000;
  shdr.sh_info = 0x00000000;
  shdr.sh_addralign = 0x0000000000000004;
  shdr.sh_entsize = 0x0000000000000000;
  fwrite(&shdr, sizeof(shdr), 1, f);

  fclose(f);
  f = NULL;

  /* Show file size */
  printf("Size of a.out: %lu bytes\n", HEADER_SIZE + sizeof(code) + sizeof(SH_STR_TBL) + FOOTER_SIZE);
  fflush(stdout);

  /* Give execution permission */
  chmod("a.out", 0755);
  /* Execute created binary */
  system("./a.out");

  return EXIT_SUCCESS;
}

上記のプログラムから a.out を生成し,objdump で逆アセンブルしてみると,以下のようになる.

$ gcc -O2 gen_hello_x64.c -o gen_hello_x64
$ ./gen_hello_x64
Size of a.out: 380 bytes
Hello World!
$ objdump -d a.out

a.out:     ファイル形式 elf64-x86-64


セクション .text の逆アセンブル:

0000000008000078 <.text>:
 8000078:       48 c7 c0 01 00 00 00    mov    $0x1,%rax
 800007f:       ba 0d 00 00 00          mov    $0xd,%edx
 8000084:       48 c7 c6 9d 00 00 08    mov    $0x800009d,%rsi
 800008b:       bf 01 00 00 00          mov    $0x1,%edi
 8000090:       0f 05                   syscall
 8000092:       48 c7 c0 3c 00 00 00    mov    $0x3c,%rax
 8000099:       31 ff                   xor    %edi,%edi
 800009b:       0f 05                   syscall
 800009d:       48                      rex.W
 800009e:       65                      gs
 800009f:       6c                      insb   (%dx),%es:(%rdi)
 80000a0:       6c                      insb   (%dx),%es:(%rdi)
 80000a1:       6f                      outsl  %ds:(%rsi),(%dx)
 80000a2:       20 57 6f                and    %dl,0x6f(%rdi)
 80000a5:       72 6c                   jb     0x8000113
 80000a7:       64 21 0a                and    %ecx,%fs:(%rdx)
        ...

無事に .text セクションの中身がわかるようになった. Hello World! の部分については無理に解釈されているため,でたらめなニーモニックが出力されているが,気にしなくてもよい.

まとめ

実行ファイルにはELFヘッダとプログラムヘッダが必須となるが,セクションヘッダは無くてもよい. ヘッダでエントリポイントを指定し,その位置から機械語を配置することで,実行ファイルができる.

なお,今回の記事の実行ファイル生成プログラムは以下のリポジトリにある.

また,工夫次第ではもっと小さい実行ファイルを作れるようだ. 以下の記事では終了ステータスを返すだけのx86プログラムだが,x64でも同様の手法がとれるだろう.

不思議なHello World

背景

最近になって,x64をちゃんと勉強したので,少しだけひねったHello WorldC言語で書いてみることにした. (※x64のLinux環境でしか動作しない)

const char main[] = "\x48\xc7\xc0\x01\x00\x00\x00\xba\x0d\x00\x00\x00\xbf\x01\x00\x00\x00\xe8\x0c\x00\x00\x00\x48\x81\xc6\x11\x00\x00\x00\x0f\x05\x31\xc0\xc3\x48\x8b\x34\x24\xc3Hello World!\n";

何とワンライナーである. ヘッダのインクルードが無く,main関数ではなく,main配列となっている点が通常のHello Worldと比較すると異質な点であろう.

ワンライナーではわかりづらいので,コメントを入れてみる. 要は機械語である.

const char main[] =
  // mov $0x01,%rax
  "\x48\xc7\xc0\x01\x00\x00\x00"
  // mov $0x0d,%edx
  "\xba\x0d\x00\x00\x00"
  // mov $0x01,%edi
  "\xbf\x01\x00\x00\x00"
  // callq 0x0c
  "\xe8\x0c\x00\x00\x00"
  // add $0x11,%rsi
  "\x48\x81\xc6\x11\x00\x00\x00"
  // syscall
  "\x0f\x05"
  // xor %eax,%eax
  "\x31\xc0"
  // ret
  "\xc3"
  // mov (%rsp),%rsi
  "\x48\x8b\x34\x24"
  // ret
  "\xc3"
  // String data
  "Hello World!\n";

先にタネ明かしをすると,以下を行っているだけにすぎない.

// 第1引数:ファイルディスクリプタ (1: stdout)
// 第2引数:文字列領域へのポインタ
// 第3引数:出力文字数
write(1, "Hello World!\n", 13);

このことを念頭に置き,解説を行っていく.

解説

全体像

mainというのは所詮 crt*.o 中から呼び出しされているに過ぎない. 従って,main という名前の指し示すアドレスの先に関数と同様のデータがあれば問題はない.

基本的に1つの配列に納めたかったので,コードとデータを同一の領域に置いてある. 別々にした場合,データのアドレスを取得するのが面倒になるためだ. (引数でデータのポインタを渡す等の処理が必要になるが,今回はmainを作っているため難しい)

配列 main 中の "Hello World!\n" より前は実行するコードであり,"Hello World!\n" は当然データである. 後は,コード中のいずれかの位置で,その地点のアドレスを取得し, "Hello World!\n" 領域のアドレスを取得すればよい.

システムコール

x64におけるシステムコールでは各レジスタが以下のように役割を果たす. 今回利用するのは第三引数まで.

レジスタ 役割
rax システムコール番号
rdi システムコールの第1引数
rsi システムコールの第2引数
rdx システムコールの第3引数

今回利用する write システムコールC言語APIは以下の通り.

size_t write(int fd, const void *buf, size_t count)

引数 役割
fd ファイルディスクリプタ
buf 出力データのサイズ
count 出力文字数

以上より,レジスタを以下の状態にすることを目指す.

レジスタ 中身
rax 1 (writeシステムコールの番号は1)
rdi 1 (stdoutは1)
rsi Hello Worldへのアドレス
rdx 13 (“Hello World!\n” の文字数は13)

現在位置の取得

Hello World! 文字列アドレスの取得のために,実行しているコードの位置を取得する必要がある. 現在位置はレジスタripに入っているが,直接値を取り出すことはできない. したがって,

  1. call命令を利用し,rspレジスタの管理アドレスにripの値を書き込ませる.
  2. rspが書き込んだ領域からripを取得(mov (%rsp),%rsi

という手段で取得する.

コードまとめ

ここまでの話をまとめると,配列の中身は以下の通りになる.

命令 動作
mov $0x01,%rax raxに1を格納
mov $0x0d,%edx rdxの下位32bitに0x0d(0x14)をセット
mov $0x01,%edi rdiの下位32bitに0x01をセット
callq 0x0c 12bbyte先を関数として呼び出し
add $0x11,%rsi 0x0fをrsiに加算.この命令位置から17byte先が "Hello World!\n"
syscall システムコール
xor %eax,%eax mainの返り値を0に設定(eaxは関数の返り値)
ret main関数のreturn
(%rsp),%rsi 呼出元のアドレスをrsiに格納
ret 呼出位置アドレス取得関数のreturn(add $0x0f,%rsiに復帰)
"Hwllo World!\n" 文字列データ

その他いろいろ

通常,関数の返り値はraxに格納するものだが,面倒なので今回は直接rsiに放り込むという手段を取っている. 自分でコードを書く分には,x64の呼出規約を無視しても構わないだろう.

まとめ

この記事では,少し奇妙な形のC言語Hello Worldを紹介した. 一見,難解に見えるHello Worldのコードであるが,わかってしまえば大したことはない.

今回はx64用のコードを紹介した. なので,Wandboxに貼り付ければ実行できる.

x86については読者の課題としよう(笑)

Javaで簡潔にディープコピーを行う

はじめに

発端は以下の一連のツイート.

Javaのオブジェクトのコピーといえば clone() メソッドを用いるものである. しかし,標準ライブラリのArrayListでは以下のどちらの手段を用いても,シャローコピーとなるようだ.

// list01 の型は ArrayList<Hoge>
ArrayList<Hoge> list02 = (ArrayList<Hoge>) list01.clone();
ArrayList<Hoge> list03 = new ArrayList<>(list01);

これを解消するためには,一々要素のコピーを行う必要があるだろう.

ArrayList<Hoge> list03 = new ArrayList<>(Arrays.asList(list01.stream()
        .map(elm -> {
            Hoge obj = null;
            try {
                obj = (Hoge) elm.clone();
            } catch (CloneNotSupportedException e) {
                throw new RuntimeException(e);
            }
            return obj;
        }).toArray(Hoge[]::new)));

しかし,こういうのはちょっと面倒なように思える上, ArrayList にしか適用できない. そこで,任意のオブジェクトに対して,ディープコピーを行うことを考える

ディープコピー

C# だと,オブジェクトのシリアライズ,デシリアライズを利用してコピーを行うのが一般的となっている. それをJavaで真似てやってみる.

以下のメソッドを定義する.

@SuppressWarnings("unchecked")
public static <T> T deepcopy(T obj) throws IOException, ClassNotFoundException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    new ObjectOutputStream(baos).writeObject(obj);
    return (T) new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())).readObject();
}

このメソッドは以下のように利用できる. 返り値の型はジェネリクス型推論により,引数と同一の型になるため,呼び出し側でキャストを行う必要はない.

// list01 の型は ArrayList<Hoge>
ArrayList<Hoge> list02 = deepcopy(list01);

このように簡潔にディープコピーが可能である. ただし,シリアライズ,デシリアライズを行うためには,対象のオブジェクトのクラスが Serializable インタフェースを実装しておく必要(メンバも含め)がある点には気をつけておくこと.

また,シリアライズ,デシリアライズのコストはやや高めなので,自作クラスのディープコピーは clone() メソッドを適切に実装するのがよいだろう.

サンプルコード

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IOException{
        ArrayList<Hoge> list01 = new ArrayList<>();
        list01.add(new Hoge(10));
        list01.add(new Hoge(20));
        @SuppressWarnings("unchecked")
        ArrayList<Hoge> list02 = (ArrayList<Hoge>) list01.clone();
        ArrayList<Hoge> list03 = new ArrayList<>(list01);
        ArrayList<Hoge> list04 = new ArrayList<Hoge>(Arrays.asList(list01.stream()
                .map(elm -> {
                    Hoge obj = null;
                    try {
                        obj = (Hoge) elm.clone();
                    } catch (CloneNotSupportedException e) {
                        throw new RuntimeException(e);
                    }
                    return obj;
                }).toArray(Hoge[]::new)));
        ArrayList<Hoge> list05 = deepcopy(list01);

        list01.get(0).setValue(100);

        System.out.println("list01: Original");
        list01.stream().mapToInt(t -> t.getValue()).forEach(System.out::println);
        System.out.println("list02: Shallow copy");
        list02.stream().mapToInt(t -> t.getValue()).forEach(System.out::println);
        System.out.println("list03: Shallow copy");
        list03.stream().mapToInt(t -> t.getValue()).forEach(System.out::println);
        System.out.println("list04: Deep copy");
        list04.stream().mapToInt(t -> t.getValue()).forEach(System.out::println);
        System.out.println("list05: Deep copy");
        list05.stream().mapToInt(t -> t.getValue()).forEach(System.out::println);
    }

    @SuppressWarnings("unchecked")
    public static <T> T deepcopy(T obj) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new ObjectOutputStream(baos).writeObject(obj);
        return (T) new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())).readObject();
    }
}

/**
 * Cloneable は 要素を1つずつ追加するdeep copyの例のため
 * Serializable は シリアライズ / デシリアライズによる deep copyのため
 */
class Hoge implements Cloneable, Serializable {
    private static final long serialVersionUID = 1145141919810L;
    private int value;

    Hoge(int value) {
        this.value = value;
    }

    int getValue() {
        return value;
    }

    void setValue(int value) {
        this.value = value;
    }

    @Override
    public Hoge clone() throws CloneNotSupportedException {
        Hoge obj = (Hoge) super.clone();
        obj.value = value;
        return obj;
    }
}

まとめ

Javaの標準ライブラリの中にはディープコピーが楽ではないものもある.

リアリアズ,デシリアライズ用いてdeep copyを行うためには以下のようなメソッドを用意するとよい.

@SuppressWarnings("unchecked")
public static <T> T deepcopy(T obj) throws IOException, ClassNotFoundException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    new ObjectOutputStream(baos).writeObject(obj);
    return (T) new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())).readObject();
}

Vimのあまり使われない機能について~暗号化と印刷~

はじめに

この記事はVim Advent Calendar 2016の15日目の記事です.

今回はVimであまり知られていない,使われていない機能であると思われる暗号化機能について書きたいと思います.

暗号化機能

Vimテキストエディタですが,単なるテキストエディットだけに留まらず,テキストファイルを暗号化する機能も備わっています. 編集したテキストファイルを保存するとき,ユーザが指定したキーによって暗号化し,再度そのファイルオープンするときにキーの入力を求めます.

f:id:koturn:20161214012205g:plain

そして,オープン時に入力されたキーによって復号化を行うので,正しいキーであれば元のテキストが表示され,異なったキーであればデタラメなテキストが表示されるようになっています.

利用可能な暗号化方式

VIm 8.0の時点では,以下の表に示す3つの暗号化方式が利用可能となっています. 各暗号化方式についての詳細は :h cryptmethod を参照しましょう.

暗号化方式 強度 バージョン
zip 7.2 以前
blowfish 中強度(脆弱性あり) 7.3 以降
blowfish2 中強度 7.4.401 以降

cryptmethod オプションに上記のいずれかの値をセットすることで,暗号化方式を指定することができます.

デフォルトでは zip の指定となっているのですが,最新のVimを利用しており,古いバージョンのVimで開き直すことを考えないのであれば, blowfish2 を指定するべきでしょう. 利用できる最新の暗号化方式を指定していない場合,暗号化するときにVimからワーニングメッセージが出て邪魔になります.

あらゆる環境を想定していて,その環境のVimで利用可能な最新の暗号化方式を利用したいという奇特な方は,以下の設定を.vimrcに記述するとよいでしょう.

if has('cryptv')
  if v:version > 704 || v:version == 704 && has('patch401')
    set cryptmethod=blowfish2
  elseif v:version >= 703
    set cryptmethod=blowfish
  else
    set cryptmethod=zip
  endif
endif

blowfish2blowfish が利用可能かどうかはそれぞれ has('crypt-blowfish2')has('crypt-blowfish') で確認することもできるのですが,これらの条件が利用できるようになったのは Vim7.4.1009 以降でおり,blowfish2 が利用可能になったバージョンよりも後です. したがって,Vimのバージョンによって分岐する方がより古いVimに対応しているといえるでしょう.

暗号化を行う

さて,実際に暗号化を行ってみましょう.

コマンド経由

現在のファイルを暗号化したい場合,ファイル保存前に :X コマンドを実行し,暗号化キーを指定してください. よくあるパスワード入力のように,確認のため2回同じキーを入力する必要があります.

f:id:koturn:20161214012209g:plain

オプション経由

オプション key の値に暗号化に用いるキーを設定し,その後にファイルを保存することでも暗号化できます. ただし, .vimrc に記述すると,暗号化キーが筒抜けなので,通常は記述するべきではないでしょう. (.vimrcに記述した場合は,何もしなくても常に暗号化ができるというメリットはあります) 利用する場合は,保存前に :set key=hogehoge といった感じで,手動でオプションに値を設定しましょう.

オプション key は扱いがやや特殊であり, set keyecho &key としても,設定値を確認することはできません. また,ヒストリにも set key= までしか残らず,暗号化キーが分からないように細工されています.

key オプションに値を設定する場合は,暗号化キーの入力が一度で済みますが,その分typoに気付かなかった場合,ファイルの復号化が難しくなるでしょう. 僕としては, :X コマンドを利用して暗号化キーを設定するとよいと思います.

暗号化を解除する

一度暗号化したファイルは,再度オープンし,保存し直しても,暗号化されたままです. 暗号化を解除したくなった場合は,空のキーを設定し直すとよいです.

:X コマンドを用いる場合は,キーを何も入力せずに <CR> だけを2回,key オプションを利用する場合は, set key= としましょう.

印刷機能

実はVimには印刷機能が備わっています. 印刷は簡単で, :hardcopy コマンドを実行するだけです.

印刷設定

印刷設定に関するオプションは7種類あり,以下の通りです.

オプション 機能
printdevice :hardcopy! のようにbang付きで実行されたときのプリンタを指定
printencoding 印刷に用いるエンコーディングを設定
pritfont Ascii文字の印刷に用いるフォントを設定
printheader 印刷のヘッダを設定. statusline と同じ書式で指定する
printmbcharset マルチバイトの文字セットを指定
printmbfont マルチバイトの印刷に用いるフォントを設定
printoptions 余白,行番号の有無,折り返しの有無等を設定

上記7つの内,基本的に設定しておくべきなのは以下の3つでしょう.

  1. printfont
  2. printmbfont
  3. printoptions

あとは,自動設定に任せる or 用いない等で設定しなくても大丈夫でしょう.

僕はWindowsユーザなのですが,以下のように設定しています.

set printoptions=number:y,header:0,syntax:y,left:5pt,right:5pt,top:10pt,bottom:10pt
set printfont=Consolas:h8
set printmbfont=r:MS_Gothic:h8,a:yes

標準では行番号の印刷がoffになっているので, number:y を入れておくとよいでしょう. また,僕はヘッダ行を必要としないため header:0 を付与しています.

上記の設定にはありませんが,縦向きor横向き,印刷する用紙の大きさ等を設定することもできます.

印刷設定

印刷には,設定中のカラースキームが一部反映されます. そのため,実際に印刷してみると,悲惨な結果になっていることがあります.

f:id:koturn:20161214012216p:plain

f:id:koturn:20161214012223p:plain

仮想プリンタ等を導入し,実際に出力してみて,どうなっているかを確認してから,紙に出力するとよいでしょう.

まとめ

この記事ではVimの暗号化機能と印刷機能について紹介しました. 暗号化機能の利用シーンとしては,ギョームでちょっと秘密にしておくべきファイルを扱うとき,などが考えられるでしょうか. 印刷機能は何かの用事があってコードを印刷したいときに利用できると思います.

なお,neovimでは暗号化機能は削除されているため,利用することはできません. この点には気を付けましょう.

denite.nvimのsourceを作ってみる

はじめに

unite.vim に替わる新しいプラグインとして,denite.nvimが開発されている. まだまだ開発途上ではあるものの,じわじわとunite.vimから移行する人が見受けられる. denite.nvimはneovimだけでなく,Vim 8.0以降であれば利用可能であるのも魅力の1つであるだろう.

そこで,(ゴミ)プラグイン作者の一人として,denite-sourceの作成方法について記したいと思う.

source / kind作成

サンプルとして作るもの

何よりもまず,実例を示すのが早いだろう. サンプルとして,以下のようなsourceを作成してみることにしよう.

  • sourceの引数で指定されたディレクトリ(複数指定可)のファイル一覧を表示
  • sourceの引数が指定されなかった場合はホームディレクトリのファイル一覧を候補として表示
  • 選択したファイルの行数を print() する

ファイル構成

以下の構成でプラグインディレクトリを作成する. 今回は denite-source のみのプラグインであるため, plugin/autoload/ も存在しない. 同じものは GitHub: koturn/vim-denite-sampleにも置いてある.

vim-denite-sample/
|
+-.gitignore
|
+-README.md
|
+-LICENSE
|
+-rplugin/
  |
  +-python3/
    |
    +-denite/
      |
      +-kind/
      | |
      | +-sample.py
      |
      +-source/
        |
        +-sample.py

コード

vim-denite-sample/rplugin/pyhton3/denite/source/sample.py

本体同梱のsourceを真似て,sourceを作成する. まず, from .base import Base のようにBaseクラスをインポートし,それを継承して, Source クラスを作成する. Source クラスには必要なメソッドを実装するとよい. 特に, gather_candidates メソッドは候補取得のためのメソッドなので,必ず実装することになる.

# -*- coding: utf-8 -*-
# FILE: sample.py
# AUTHOR: koturn <jeak.koutan.apple@gmail.com>
# License: MIT License

from .base import Base
import glob
import itertools
import os


class Source(Base):
    def __init__(self, vim):
        #  このvimという引数はVimとPythonで相互にやりとりするためのインタフェース
        # :help pyth を参照すれば,おおよそのことが記述してある
        super().__init__(vim)
        # Denite xxx の xxx に相当する部分
        self.name = 'sample'
        # kind名を指定
        self.kind = 'sample'

    def on_init(self, context):
        '''
        このsourceが指定されて起動されたときに呼び出される.
        元のバッファのファイルタイプの取得などに利用する.
        今回は必要ないので,とりあえずechomsgしておく
        '''
        # print() は :echomsg と同等
        print('on_init')
        # filetypeなどの値は,以下のように context のキーとして生やす
        # selfのメンバにするのはよくない
        # context['__filetype'] = self.vim.eval('&filetype')

    def on_close(self, context):
        '''
        Denite終了時に呼び出される.
        '''
        # コンストラクタ以外では,self.vim を参照して,Vimインタフェースを利用する
        print('on_close')

    def gather_candidates(self, context):
        '''
        候補の取得を行う関数
        '''
        # sourceの引数context['args']はユーザがDenite実行時に以下のように : 区切りで指定する
        #   Denite sample:arg1:arg2:arg3
        dirpaths = ['~'] if len(context['args']) == 0 else context['args']
        print(dirpaths)
        candidates = filter(lambda path: os.path.isfile(path),
                        itertools.chain.from_iterable(
                            map(lambda dirpath: glob.glob(os.path.expanduser(dirpath) + '/*'), dirpaths)))
        # 辞書を返す
        #   word: 候補欄に表示される文字列
        #   アクション側で利用するための情報を増やしたい場合は 'action__xxx'
        #   という名前のキーを利用すること
        return list(map(
            lambda candidate: {
                'word': candidate},
            candidates))

vim-denite-sample/rplugin/pyhton3/denite/kind/sample.py

denite-kindはアクション部分を記述するものである. denite-kindに関しても,sourceと同様に from .base import Base として,Base クラスをインポートする. そして,それを継承した Kind クラスを作成し,必要なメンバ,メソッドを実装する. メンバの中で特に重要なのが default_action であり,これは <CR> を押下したときのアクションに相当する.

# -*- coding: utf-8 -*-
# FILE: sample.py
# AUTHOR: koturn <jeak.koutan.apple@gmail.com>
# License: MIT License

from .base import Base

class Kind(Base):
    def __init__(self, vim):
        super().__init__(vim)
        # kindの名前.この名前をsourceで指定することにより,関連付けられる
        self.name = 'sample'
        # デフォルトのアクション(候補上で<CR>を押下したときに実行するアクション)を指定する.
        # self.default_action = 'xxx' と指定すると,このクラスのメソッド:
        # action_xxx() が呼び出される
        self.default_action = 'sample'

    def action_sample(self, context):
        '''
        コンストラクタで指定したように,デフォルトのアクションを記述する関数
        '''
        for target in context['targets']:
            filepath = target['word']
            print(filepath + ' has ' + str(sum(1 for line in open(filepath, encoding='utf-8'))) + ' lines')

    def action_preview(self, context):
        '''
        候補上にカーソルが移動する度に呼び出される関数
        :Denite -auto-preview ... のように -auto-preview オプションを付加すると,previewモードになる
        付加しない場合でも,以下のキーを押下することでpreview動作を行うことは可能
          insertモード: <C-v>
          normalモード: p
        return Trueとしないと,deniteが終了する
        '''
        # context['targets'] に source側のgether_candidatesの返り値が格納されている
        filepath = context['targets'][0]['word']
        print(filepath + ' has ' + str(sum(1 for line in open(filepath, encoding='utf-8'))) + ' lines')
        return True

サンプルの使い方

以下のように起動すると,ホームディレクトリのファイル一覧が表示される. 選択したファイルの行数が print() に渡されるので, :message 等で確認するとよい.

:Denite sample

次のように,sourceの引数とディレクトリを渡すと,ホームディレクトリではなく,そのディレクトリのファイル一覧が表示される.

:Denite sample:~/Desktop

引数を複数指定することも可能だ.

:Denite sample:~/Desktop:~/Documents/

テンプレート

最低限のdenite-sourceを実装するのであれば,以下のようなテンプレートを用意しておくと楽になるだろう. on_initon_close については,必ずしも利用するわけではないので,コメントアウトしてある.

denite-source のテンプレート

# FILE: xxx.py
# AUTHOR: xxx <xxx.yyy.zzz@gmail.com>
# License: MIT license

from .base import Base


class Source(Base):
    def __init__(self, vim):
        super().__init__(vim)
        self.name = 'xxx'
        self.kind = 'xxx'

    # def on_init(self, context):
        # TODO

    # def on_close(self, context):
        # TODO

    def gather_candidates(self, context):
        # TODO: Following code is a sample
        candidates = ['apple', 'banana', 'cake']
        return list(map(
            lambda candidate: {
                'word': candidate},
            candidates))

denite-kind のテンプレート

# FILE: sample.py
# AUTHOR: xxx <xxx.yyy.zzz@gmail.com>
# License: MIT license

from .base import Base


class Kind(Base):
    def __init__(self, vim):
        super().__init__(vim)
        self.name = 'xxx'
        self.default_action = 'xxx'

    def action_sample(self, context):
        # TODO

    # def action_preview(self, context):
        # TODO

余談

実は,unite.vimにもdeite-sourceが実装され,unite-sourceをdeniteから利用することが可能になった. 以下のようにして,deniteからuniteを利用する.

" xxx がunite-source名
:Denite unite:xxx

このため,既にプラグインにunite-sourceを実装している場合は,改めて同等のdenite-sourceを実装し直す必要はないということだ. しかし,ユーザとしてはdenite-sourceも提供されていた方が気持ちが良いと思われる.

ちなみに,denite-sourceはPythonであるため,当然バイトコードが生成される. そのため, .gitignore に,

*.py[cod]

と追記しておくのがよいだろう.

おまけ

実はちょっとした自作プラグインに取り込んである.

:Denite mplayer

とすることで,デフォルトでは ~/ 以下のファイルを再帰的に表示し,mplayer を用いて,選択したファイルを再生するようになっている.

デフォルトディレクトリは g:mplayer#default_dir を設定することで変更可能であるし,

:Denite mplayer:~/Music/

のように指定してもよい.

また,プレビュー機能にも対応しているため,

:Denite mplayer:~/Music/ -auto-preview

とすることで,Deniteバッファ上で,カーソル移動の度にカーソル下のファイルを再生できる.

このように,ちょっとした自作プラグインに導入することで,格段に便利になる.

まとめ

この記事では,最も単純なdenite-sourceの作成方法を示した. プラグイン作成初心者のdenite-source作成の足掛かりとなれば幸いである.

ただし,あくまで,最も初歩的な作成方法について述べただけであり,触れていない項目の一例として matcherやsorter,converterといった話もある. それらの機能についてはdocやdenite.nvim自体のソースコードを参照して欲しい.

なお,deniteは開発が活発なプラグインであるため,数ヶ月後にはこの記事の内容が古いものとなっている可能性は十分にある.

Osaka.vim #8 に行ってきた

はじめに

昨日,10/29(土)にOsaka.vim #8に行ってきました. 前回は大学の後輩を誘って参加しましたが,今回は都合が悪かったので参加できず...残念.

やったこと

denite.nvimの勉強

最初はShougo/denite.nvimについて勉強するかーと思って,denite.nvimのhelpと本体付属のsourceを読んでました. 少し前に自作プラグイン用のdenite.nvimのsourceを書いたときにちょっと勉強してたのもあって,これ自体はそこそこに終えました. で,以前書いたsourceの作法としてマズかった部分を修正しました.

プラグインの修正

ここ最近手を加えている自作プラグインkoturn/vim-mplayerをいじっていました. このプラグインVimからmplayerを起動,操作するプラグインです. 2年前に作ったプラグインであり,Osaka.vim #2のときに機能作り込みしてた記憶があります. 2年前に基本機能を作って寝かせ,1年前にunite sourceやctrlpの拡張,fzfと連携できるようにして寝かせ,今年はjob対応,みたいな感じで,なんか1年周期でいじっているプラグインですね.

いじったと言っても,機能追加は特になく,内部をスッキリさせたぐらいですね. あらかたの修正はここ数週間で既に終えていたというのもありますね.

Osaka.vim #8までの「ここ数週間での修正」というのは,

って感じです.

以前は,:MPlayer hoge.mp3 みたいなコマンド経由で操作するだけしかできないようにしていましたが,オブジェクト指向チックにするに伴い,複数のmplayerプロセスを操作することができるようになりました.

以下のような感じで,他のプラグインから利用可能になりました. (mplayer利用するゴミプラグインはいくつか作ったので,この変更は自分のためですね) 単純なmplayer操作プラグインにライブラリとしての側面を追加した形,って感じですかね. 以下の形で利用できます.

" mplayerのインスタンス生成
let s:mplayer = mplayer#new()
" mplayerプロセス起動と指定ファイル再生
call s:mplayer.play('~/Music/hoge.mp3')
" 音量レベルを70%に
call s:mplayer.set_volume(70)
" 終了(プロセス終了)
" call s:mplayer.stop()

help書き

koturn/vim-mplayerのhelpを面倒で書いてなかったので,そろそろ書くか~って思って書いていました. helpをゼロから書くのは面倒なので,LeafCage/vimhelpgeneratorを利用して,雛形を生成し,ごにょごにょと書いてました. 上記のhelp雛形生成プラグインは,今回のOsaka.vim #8 参加者の一人である@LeafCageさんのベンリプラグインですね.

英語力無いマンであり,それなりに書くことがあったので,helpはまだ書き終えていません()

出てきた話とか

とりあえず,箇条書きで.

前回と同様,電源タップを2個持っていったのですが,みんな持ってきてて,むしろ必要なかったぐらいでした. fzf関連の話で自分のブログが検索の上の方に出てきたのは面白かったです. 他にも話があった気がするけど,思い出しながら書くと抜けがあるので,あとで追記する可能性あります.

あと書き

自分のもくもく作業としては,そこそこ進捗はあったと思います. 後半に@kozo2さんが「LTというか質問という形で...」と前に出て,対話するスタイルで話を始めたのですが,バンバンと各個人の知識が出てくるのはよかったな~と思いました. また次回も参加したいですね.