Vim scriptのautoloadについて

本日の爆音駆動BGMはこちら♫ open.spotify.com

それなりに規模の大きいVimのプラグインを見ているとautoloadディレクトリが見つかることが多いと思う。
今回はautoloadの挙動を軽くまとめます。

仕様確認

とりあえずhelpで仕様を調べる。

help autoload

autoload scriptの仕様ではautoloadディレクトリ内にVim scriptファイルを配置すると動的に読み込みに行ってくれる。

autoload scriptには関数や変数の命名規則があって、次のように関数や変数の前に名前階層をつけて定義してあげる必要がある。

" ~/.vim/autoload/dir1/dir2/hello.vim の場合 
function dir1#dir2#hello()
  " 処理
endfunction

let dir1#dir2#hello_variable = ""

autoloadの挙動

では試しに ~/.vim/autoload/hello.vim というファイルを作成して、次の関数を定義する。

function hello#world()
  echo "Hello autoload function!"
endfunction

これでautoloadの仕様で関数の実行時に動的にファイルを読みに行ってくれる。
いったんvimを閉じて再度vimを立ち上げるて関数を呼び出してみると確認できる。

:call hello#world()

Hello autoload function!

これで一応autoload scriptが読み込まれているのが確認できた。
が、動的に読み込まれていることも確認してみたいと思う。

" この行を追加
echo 'load autoload script hello.vim'

function hello#world()
  echo "Hello autoload function!"
endfunction

そして再度vimを開き直す。

f:id:narinymous:20180520215458p:plain

vimを開き直した時点ではechoによるメッセージが出ていないのでまだファイル自体が読み込まれていないことがわかる。
関数を実行した時点で次のようにechoメッセージが出力される。

:call hello#world()

load autoload script hello.vim
Hello autoload function

f:id:narinymous:20180520215507p:plain

関数の実行時にファイルが読み込まれたことで1つ目のechoメッセージが、次に関数の実行によるechoメッセージが確認できました。
プラグイン管理ツールを導入している場合は各プラグインのautoloadディレクトリも対象に含まれるようになる。
なので、プラグインの実装では単にプロジェクトのルート階層にautoloadディレクトリを配置しておけばいい。

Vim scriptでファイル探索

本日の爆音駆動BGMはこちら♫ open.spotify.com

最近Vimプラグインを実装しはじめました。
実装をしている際に現在開いているファイルから親ディレクトリをどんどん辿り、任意のファイルを探索し、見つかったファイル内の文字列リテラルで囲まれた文字を抽出する処理を実装しました。
今回はファイル検索に関する実装部分のTipsをまとめます。

親ディレクトリを辿ってファイルを見つける

Vimで開いたファイルは、その開いたファイルの位置がVimのカレントディレクトリとして記録されます。vim-rooterというVimプラグインではプロジェクトのルート位置にカレントディレクトリを移動するために.git/などが存在する位置を探索してプロジェクトのルートに移動しています。

vim-rooterと同じように.gitディレクトリが存在する位置を探索するにはどうしたらいいでしょう。
やり方は色々あると思いますが、僕なりに実装すると次のような手順になります。

  1. 現在開いているファイルからカレントディレクトリを取得
  2. カレントディレクトリ内で.git/が存在するか検索
  3. .gitが存在しなかったら上の階層へカレントディレクトリを移す
  4. 2と3を繰り返す
  5. 存在しなかったら開いているファイルが存在するディレクトリに戻しておく

これを率直に実装すると次のようになる。

let s:current_dir   = expand("%:p:h")
let s:file_patterns = [".git"]

" ファイルを指定せずにvimを起動したら処理を抜ける
if @% == ""
  finish
endif

" 探索したいファイル名群とマッチするかどうか判定
function s:IsMatchPattern(file)
  for pattern in s:file_patterns
    return a:file == pattern
  endfor
endfunction

" ファイルの探索処理
function s:FindFile(dir)
  let dir = (a:dir)[1:]
  let normalfiles = split(glob(dir . "/*"), "\n")
  let dotfiles    = split(glob(dir . "/\.*"), "\n") 
  for file in normalfiles + dotfiles
    let filename = fnamemodify(file, ":t")
    if s:IsMatchPattern(filename)
      return file
    endif
  endfor
endfunction


let s:count = 0
function s:Main()
  while s:count < len(split(s:current_dir, "\/"))
    let file = s:FindFile(execute("pwd"))
    if !empty(file)
      break
    else
      execute "lcd ../" 
      let s:count += 1
    endif
  endwhile

  if empty(file)
    cd %:h
  endif
endfunction

call s:Main()

このスクリプトを.vimrcに書き足すか、別ファイルに切り出してsourceで読み込んであげると動きます。
スクリプト上部の次の部分はファイルを指定せずにvimを起動した場合は処理を抜ける部分です。
単に vim だけ起動した場合はここを通ります。

if @% == ""
  finish
endif

次にスクリプト下部のMain関数の実装です。
開いたファイルのカレントディレクトのパスを/で分割しリストに変換しています。
このリストの数だけ繰り返しでs:FindFile関数の実行で探索し、対象のディレクトリ内に.gitが見つからなかったらカレントディレクトリを1階層上に移り再度探索を開始します。
何も見つからなかった場合は、開いていたファイルのカレントディレクトリへ移動して終了します。

ディレクトリを移動する際はcdまたはlcdコマンドが使えます。
cdの場合、vsplitなどでウィンドウ分割しているウィンドウ全てのカレントディレクトリが変更されます。
lcdの場合は実行したウィンドウのみのカレントディレクトリが変更されます。

let s:count = 0
function s:Main()
  while s:count < len(split(s:current_dir, "\/"))
    let file = s:FindFile(execute("pwd"))
    if !empty(file)
      break
    else
      execute "lcd ../" 
      let s:count += 1
    endif
  endwhile

  if empty(file)
    cd %:h
  endif
endfunction

call s:Main()

次にファイルの探索処理の実装を見ていきます。

" ファイルの探索処理
function s:FindFile(dir)
  let dir = (a:dir)[1:]
  let normalfiles = split(glob(dir . "/*"), "\n")
  let dotfiles    = split(glob(dir . "/\.*"), "\n") 
  for file in normalfiles + dotfiles
    let filename = fnamemodify(file, ":t")
    if s:IsMatchPattern(filename)
      return file
    endif
  endfor
endfunction

引数にはMain関数内で渡していたpwdコマンドによりカレントディレクトリのパスが入ってきます。
pwdコマンドはカレントディレクトリの先頭に改行コード(\n)を含んでいるため、次のように文字列の先頭以降を取得しています。

  let dir = (a:dir)[1:]

そして、カレントディレクトリ内の全てのファイルをリストで連結する処理が次のようになります。

  let normalfiles = split(glob(dir . "/*"), "\n")
  let dotfiles    = split(glob(dir . "/\.*"), "\n") 

通常のファイルと隠しファイルをglob関数で展開したものをリストに分割して取得しています。
この2つのリスト変数を連結し最後はfor文によりパターンにマッチするファイルを見つけ出しています。
このs:FindFile関数内に存在する次の部分のfnamemodify関数は第一引数に与えたファイル名を第二引数に与えたファイル修飾子に従ってファイル名を修飾する関数です。
フルパスからファイル名だけを抽出する際は次のようになります。

  let filename = fnamemodify(file, ":t")

最後は's:IsMatchPattern'関数で引数に渡ってくるファイル名と探しているファイルの比較の処理で終わりです。

ファイル/パス情報の取得

fnamemodifyに関しては次の結果を見るとわかりやすいと思います。

let filename = "/Users/naritatakuya/workspace/vim/target.txt"
echo fnamemodify(filename, ":t")
" => target.txt
echo fnamemodify(filename, ":t:r")
" => target
echo fnamemodify(filename, ":e")
" => txt
echo fnamemodify(filename, ":p")
" => /Users/naritatakuya/workspace/vim/target.txt

cd ..
echo fnamemodify("target.txt", ":p")
" => /Users/naritatakuya/workspace/target.txt

最後の行の出力結果はカレントディレクトリを変更した場所に第一引数のファイル名が連結された形になっているので、実在しないファイルになっています。

また、スクリプト中に登場したexpand関数も似た組み込み関数ですが、こちらは現在開いているファイルを対象にパス情報を取得します。

echo expand("%:p")
" => /Users/naritatakuya/workspace/vim/target.txt

echo expand("%:p:h")
" => /Users/naritatakuya/workspace/vim

echo expand("%:p:t")
" => target.txt

echo expand("%:e")
" => txt

cd ..
echo expand("%:p")
/Users/naritatakuya/workspace/vim/target.txt

fnamemodifyと違って開いているファイルが対象なため、おそらく実在しないパスは取得されないはずです。
fnamemodifyはカレントディレクトリとは違う場所に存在するパスを操作する際などに使用できそうです。

Vim script中に出てくるパス関連の修飾子は次のhelpを参考にしてください。

:h filename-modifiers

IEEE754における浮動小数点数の2進数表現

10.75という10進数の数値を倍精度数(64bit)の2進数で表現するとどうなるでしょう。
結果は以下のようになる。

010000000010010110000000000000000000000000000000000000000000000

まずIEEE754という標準規格についてまとめる。
今回は倍精度数に焦点を当てるので64bitの表現とする。
下図のように最上位(63bit)は符号部, 52bit ~ 62bitは指数部, 0bit ~ 51bitは仮数部として割り当てられる。
 

https://upload.wikimedia.org/wikipedia/commons/a/a9/IEEE_754_Double_Floating_Point_Format.svg
参考: Wikipedia - The memory format of an IEEE 754 double floating point value.  
 

ここで10.75の整数部は10, 小数部は0.75だが整数部の10は2進数表現だと10101となるのは容易に理解できる。
小数部の0.75の部分は以下のように計算すると2進数表現を取得できる。

> 0.75 * 2
1.5
> 0.5 * 2
1
> 0.0 * 2
0
> 0.0 * 2
0
> 0.0 * 2
0
......

小数部分を2倍していき、整数の部分だけ取り出す。
すると結果は110000000000....0 (50bitから0bitまで0が続く) となる。

ここで整数部分と小数部分を.で繋げて以下のように仮に表現する。

1010.110000000000....0

これを正規化すると以下のようになる。
23は仮表現

1.010110000000000....0 * 2^3

この正規化により仮数部と指数部を表現できる。
まず仮数部は52bitで表して以下のようになる。
このとき、0以外は整数部分は必ず1になるので省略される。

010110000000000000000000000000000000000000000000000

そして仮表現として23で表した指数部は11bitで以下のようになる。

10000000010

この数値はどういうことかというと、指数部はイクセス表現という表し方をするためこうなる。
イクセス表現とはこの11bitで表せる範囲を中央で分割して中央からの差分により指数を表す表現のこと。
そのため3bitシフトした値は以下のように計算できる。

   01111111111
+) 00000000011 
---------------
   10000000010

仮数部と指数部が得られたのでIEEE754で表現すると以下のようになる。
(可読性をよくするため符号部, 指数部, 仮数部を空白で区切っている)

0 10000000010 010110000000000000000000000000000000000000000000000

以上が10.75のIEEE754の2進数表現になる。

この2進数を10進数に戻すときは今までの逆を行う。
まず仮数部(010110....0)の計算は以下のようになる。

2-2 + 2-4 + 2-5 + 1 = 1.34375

最後に+1しているのは仮数部で省略した1を戻すため。
仮数部の値が得られたら最後に指数部で3bitシフトしたぶんの計算を戻す演算を行う。

2-2 + 2-4 + 2-5 + 1 = 1.34375 * 23 = 10.75

これで元の10進数の数値を得られることができた。

gRPCのデバッグにはevansを使うと便利

gRPCクライアントevans
gRPC client: evans

  

先日会社のエンジニアブログでgRPCに関する以下の記事を書いていた。

blog.engineer.adways.net

blog.engineer.adways.net

gRPCを使うメリットや実装方法に関することを書きまとめ、ある程度の知識も得られたと思っている。
ある程度gRPCに関する実装に慣れてきたところで、gRPCもHTTPのようにcurlでデバッグできないのだろうか?と思っていたところevansというOSSを発見した。
今回はgRPCのデバッグツールevansに関することを軽くまとめる。

会社のブログに載せた個人のGithubリポジトリをそのまま流用してデバッグ手法を試みる。

github.com

このリポジトリではKotlinでSpring bootを使用し、gRPCサーバをポート6565で公開するように実装している。
言語やフレームワークは特にデバッグ時には関係ないので前提として知っておくのは起動しているポート番号が6565であるということだけでいい。

デバッグに必要なのはgRPCのプロトコル定義ファイルのみ。
さっき載せたリポジトリ内のプロトコル定義ファイルの内容は以下となる。

syntax = "proto3";

package taskstore;

option java_multiple_files = true;
option java_outer_classname = "TaskStore";
option java_package = "com.example.demo.taskstore";

service Taskstore {
  rpc GetTasks(GetTasksRequest) returns (GetTasksResponse) {}
  rpc AddTask(AddTaskRequest) returns (AddTaskResponse) {}
  rpc UpdateTask(UpdateTaskRequest) returns (UpdateTaskResponse) {}
  rpc DeleteTask(DeleteTaskRequest) returns (DeleteTaskResponse) {}
}

message GetTasksRequest {
}
message GetTasksResponse {
  repeated Task task = 1;
}

message AddTaskRequest {
  string content = 1;
}
message AddTaskResponse {
  Task task = 1;
}

message UpdateTaskRequest {
  int64 id = 1;
  bool done = 2;
}
message UpdateTaskResponse {
  Task task = 1;
}

message DeleteTaskRequest {
  int64 id = 1;
}
message DeleteTaskResponse {
  bool success = 1;
}

message Task {
  int64 id = 1;
  string content = 2;
  bool done = 3;
}

evansのインストール

以下のリポジトリがevansというOSS。
curlのようにサーバにリクエストを送ることもできるし、プロトコル定義をテーブル形式で表示したりと表現力豊かなgRPCクライアント。

github.com

$ brew tap ktr0731/evans
$ brew install evans

evansでgRPCのデバッグ

以降はgRPCサーバが立ち上がっていることを前提として書く。
念のため再度言及するが、今回の僕のリポジトリではSpring bootでポート6565でgRPCサーバが起動している。

evansのhelp表示が以下。

$ evans --help

evans 0.2.8
Usage: evans [--interactive] [--editconfig] [--host HOST] [--port PORT] [--package PACKAGE] [--service SERVICE] [--call CALL] [--file FILE] [--path PATH] [--header HEADER] [PROTO [PROTO ...]]

Positional arguments:
  PROTO                  .proto files

Options:
  --interactive, -i      use interactive mode
  --editconfig, -e       edit config file by $EDITOR
  --host HOST, -h HOST   gRPC host
  --port PORT, -p PORT   gRPC port [default: 50051]
  --package PACKAGE      default package
  --service SERVICE      default service. evans parse package from this if --package is nothing.
  --call CALL, -c CALL   call specified RPC
  --file FILE, -f FILE   the script file which will be executed (used only command-line mode)
  --path PATH            proto file path
  --header HEADER        headers set to each requests
  --help, -h             display this help and exit
  --version              display version and exit

--portでターゲットとなるgRPCサーバのポートを指定し、プロトコル定義ファイルの場所を教えてあげる。

$ evans --port 6565 proto/task.proto

  ______
 |  ____|
 | |__    __   __   __ _   _ __    ___
 |  __|   \ \ / /  / _. | | '_ \  / __|
 | |____   \ V /  | (_| | | | | | \__ \
 |______|   \_/    \__,_| |_| |_| |___/

 more expressive universal gRPC client


127.0.0.1:6565> 

あまりコマンドも多くなくシンプルなのため非常に使いやすい。
コマンドの一覧表を提示しておく。

コマンド名 意味
show package packageの一覧表示
show service 選択したpackageに存在するserviceを一覧表示
show message 選択したpackageに存在するmessageを一覧表示
show rpc 選択したservice内に存在するrpcメソッドを一覧表示
desc message名 引数に与えたmessageのパラメータ名と型を表示
package package名 引数に与えたpackageを選択する
service service名 引数に与えたserviceを選択する
call rpc名 引数に与えたrpcメソッドをコールする

一応解説する。
packageを選択しないと、serviceやmessageは一覧表示できない。
serviceを選択しないと、rpcメソッドの一覧は表示できないしcallもできない。
これは当たり前だがpackageは複数存在し得るし、ひとつのpackage内に複数のserviceが存在し得るから当然のこと。
なお、callコマンドは対話型でリクエストの引数を与えることができる。
早速試してみる。

127.0.0.1:6565> show package
+-----------+
|  PACKAGE  |
+-----------+
| taskstore |
+-----------+

127.0.0.1:6565> package taskstore

taskstore@127.0.0.1:6565> show service
+-----------+------------+-------------------+--------------------+
|  SERVICE  |    RPC     |    REQUESTTYPE    |    RESPONSETYPE    |
+-----------+------------+-------------------+--------------------+
| Taskstore | GetTasks   | GetTasksRequest   | GetTasksResponse   |
|           | AddTask    | AddTaskRequest    | AddTaskResponse    |
|           | UpdateTask | UpdateTaskRequest | UpdateTaskResponse |
|           | DeleteTask | DeleteTaskRequest | DeleteTaskResponse |
+-----------+------------+-------------------+--------------------+

taskstore@127.0.0.1:6565> show message
+--------------------+
|      MESSAGE       |
+--------------------+
| GetTasksRequest    |
| GetTasksResponse   |
| AddTaskRequest     |
| AddTaskResponse    |
| UpdateTaskRequest  |
| UpdateTaskResponse |
| DeleteTaskRequest  |
| DeleteTaskResponse |
| Task               |
+--------------------+

taskstore@127.0.0.1:6565> service Taskstore

taskstore.Taskstore@127.0.0.1:6565> show rpc
+------------+-------------------+--------------------+
|    RPC     |    REQUESTTYPE    |    RESPONSETYPE    |
+------------+-------------------+--------------------+
| GetTasks   | GetTasksRequest   | GetTasksResponse   |
| AddTask    | AddTaskRequest    | AddTaskResponse    |
| UpdateTask | UpdateTaskRequest | UpdateTaskResponse |
| DeleteTask | DeleteTaskRequest | DeleteTaskResponse |
+------------+-------------------+--------------------+

taskstore.Taskstore@127.0.0.1:6565> call GetTasks
{
  "task": [
    {
      "id": 65,
      "content": "Hello"
    }
  ]
}

taskstore.Taskstore@127.0.0.1:6565> 

最後のcallコマンドでGetTasksというrpcメソッドをコールしたらしっかりjson形式ではあるが、レスポンスが確認できた。

evansはコマンドライン一発で完結できるコマンドラインモードと今回説明したインタラクティブモードというものがある。
コマンドラインモードではgRPCサーバに対して、とあるスクリプトを定期実行したい時などに使えそうだ。
インタラクティブモードはやはりgRPC APIを手動で動作確認したい場合に頻繁に使えそう。

LWJGLの環境構築

LWJGLはJVM系言語で使用できる軽量OpenGLライブラリ。
ちょっと触ってみようと思ったので、環境構築手順を記述していく。

環境構築

GradleやMavenでプロジェクト管理をするならすぐ構築できる。
以下のサイトから必要なものを選んでダウンロードする。
LWJGL - Lightweight Java Game Library

今回はGradleで構築する。
Mode の項目でGradleを選択し、他は特にいじらずDownload build.gradleというボタンを押したらbuild.gradleファイルがダウンロードされる。
このbuild.gradleはそのまま使わないので、先にGradleプロジェクトを生成する。

$ mkdir lwjgl-example
$ cd lwjgl-example
$ gradle init --type java-application
$ ls -l
total 40
-rw-r--r--  1 naritatakuya  staff   993 Apr  4 01:38 build.gradle
drwxr-xr-x  3 naritatakuya  staff    96 Apr  4 01:38 gradle
-rwxr-xr-x  1 naritatakuya  staff  5296 Apr  4 01:38 gradlew
-rw-r--r--  1 naritatakuya  staff  2260 Apr  4 01:38 gradlew.bat
-rw-r--r--  1 naritatakuya  staff   577 Apr  4 01:38 settings.gradle
drwxr-xr-x  4 naritatakuya  staff   128 Apr  4 01:38 src

生成されたら先ほどダウンロードしたbuild.gradleの内容を生成したプロジェクトのbuild.gradleに移植する。
移植後は以下のような感じになるはず。

apply plugin: 'java'
apply plugin: 'application'

repositories {
    mavenCentral()
  jcenter()
}

switch ( OperatingSystem.current() ) {
    case OperatingSystem.WINDOWS:
        project.ext.lwjglNatives = "natives-windows"
        break
    case OperatingSystem.LINUX:
        project.ext.lwjglNatives = "natives-linux"
    break
    case OperatingSystem.MAC_OS:
        project.ext.lwjglNatives = "natives-macos"
        break
}


mainClassName = 'App'

/* LWJGL settings */
applicationDefaultJvmArgs = ["-XstartOnFirstThread"]

import org.gradle.internal.os.OperatingSystem

project.ext.lwjglVersion = "3.1.6"
project.ext.jomlVersion = "1.9.9"

dependencies {
  compile 'com.google.guava:guava:21.0'
  testCompile 'junit:junit:4.12'

  /* LWJGL */
    compile "org.lwjgl:lwjgl:$lwjglVersion"
    compile "org.lwjgl:lwjgl-assimp:$lwjglVersion"
    compile "org.lwjgl:lwjgl-bgfx:$lwjglVersion"
    compile "org.lwjgl:lwjgl-egl:$lwjglVersion"
    compile "org.lwjgl:lwjgl-glfw:$lwjglVersion"
    compile "org.lwjgl:lwjgl-jawt:$lwjglVersion"
    compile "org.lwjgl:lwjgl-jemalloc:$lwjglVersion"
    compile "org.lwjgl:lwjgl-lmdb:$lwjglVersion"
    compile "org.lwjgl:lwjgl-lz4:$lwjglVersion"
    compile "org.lwjgl:lwjgl-nanovg:$lwjglVersion"
    compile "org.lwjgl:lwjgl-nfd:$lwjglVersion"
    compile "org.lwjgl:lwjgl-nuklear:$lwjglVersion"
    compile "org.lwjgl:lwjgl-odbc:$lwjglVersion"
    compile "org.lwjgl:lwjgl-openal:$lwjglVersion"
    compile "org.lwjgl:lwjgl-opencl:$lwjglVersion"
    compile "org.lwjgl:lwjgl-opengl:$lwjglVersion"
    compile "org.lwjgl:lwjgl-opengles:$lwjglVersion"
    compile "org.lwjgl:lwjgl-openvr:$lwjglVersion"
    compile "org.lwjgl:lwjgl-par:$lwjglVersion"
    compile "org.lwjgl:lwjgl-remotery:$lwjglVersion"
    compile "org.lwjgl:lwjgl-rpmalloc:$lwjglVersion"
    compile "org.lwjgl:lwjgl-sse:$lwjglVersion"
    compile "org.lwjgl:lwjgl-stb:$lwjglVersion"
    compile "org.lwjgl:lwjgl-tinyexr:$lwjglVersion"
    compile "org.lwjgl:lwjgl-tinyfd:$lwjglVersion"
    compile "org.lwjgl:lwjgl-tootle:$lwjglVersion"
    compile "org.lwjgl:lwjgl-vulkan:$lwjglVersion"
    compile "org.lwjgl:lwjgl-xxhash:$lwjglVersion"
    compile "org.lwjgl:lwjgl-yoga:$lwjglVersion"
    compile "org.lwjgl:lwjgl-zstd:$lwjglVersion"
    compile "org.lwjgl:lwjgl:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-assimp:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-bgfx:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-glfw:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-jemalloc:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-lmdb:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-lz4:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-nanovg:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-nfd:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-nuklear:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-openal:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-opengl:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-opengles:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-openvr:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-par:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-remotery:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-rpmalloc:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-sse:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-stb:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-tinyexr:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-tinyfd:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-tootle:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-xxhash:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-yoga:$lwjglVersion:$lwjglNatives"
    compile "org.lwjgl:lwjgl-zstd:$lwjglVersion:$lwjglNatives"
    compile "org.joml:joml:${jomlVersion}"
}

ウィンドウを表示してみる

以下の公式ページのGet Startedのサンプルコードを貼り付けて動かしてみる。
LWJGL - Lightweight Java Game Library

以下のコードをsrc/main/java/App.javaに貼り付ける。

import org.lwjgl.*;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.*;
import org.lwjgl.system.*;

import java.nio.*;

import static org.lwjgl.glfw.Callbacks.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.system.MemoryStack.*;
import static org.lwjgl.system.MemoryUtil.*;

public class HelloWorld {

    // The window handle
    private long window;

    public void run() {
        System.out.println("Hello LWJGL " + Version.getVersion() + "!");

        init();
        loop();

        // Free the window callbacks and destroy the window
        glfwFreeCallbacks(window);
        glfwDestroyWindow(window);

        // Terminate GLFW and free the error callback
        glfwTerminate();
        glfwSetErrorCallback(null).free();
    }

    private void init() {
        // Setup an error callback. The default implementation
        // will print the error message in System.err.
        GLFWErrorCallback.createPrint(System.err).set();

        // Initialize GLFW. Most GLFW functions will not work before doing this.
        if ( !glfwInit() )
            throw new IllegalStateException("Unable to initialize GLFW");

        // Configure GLFW
        glfwDefaultWindowHints(); // optional, the current window hints are already the default
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // the window will stay hidden after creation
        glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); // the window will be resizable

        // Create the window
        window = glfwCreateWindow(300, 300, "Hello World!", NULL, NULL);
        if ( window == NULL )
            throw new RuntimeException("Failed to create the GLFW window");

        // Setup a key callback. It will be called every time a key is pressed, repeated or released.
        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if ( key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE )
                glfwSetWindowShouldClose(window, true); // We will detect this in the rendering loop
        });

        // Get the thread stack and push a new frame
        try ( MemoryStack stack = stackPush() ) {
            IntBuffer pWidth = stack.mallocInt(1); // int*
            IntBuffer pHeight = stack.mallocInt(1); // int*

            // Get the window size passed to glfwCreateWindow
            glfwGetWindowSize(window, pWidth, pHeight);

            // Get the resolution of the primary monitor
            GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());

            // Center the window
            glfwSetWindowPos(
                window,
                (vidmode.width() - pWidth.get(0)) / 2,
                (vidmode.height() - pHeight.get(0)) / 2
            );
        } // the stack frame is popped automatically

        // Make the OpenGL context current
        glfwMakeContextCurrent(window);
        // Enable v-sync
        glfwSwapInterval(1);

        // Make the window visible
        glfwShowWindow(window);
    }

    private void loop() {
        // This line is critical for LWJGL's interoperation with GLFW's
        // OpenGL context, or any context that is managed externally.
        // LWJGL detects the context that is current in the current thread,
        // creates the GLCapabilities instance and makes the OpenGL
        // bindings available for use.
        GL.createCapabilities();

        // Set the clear color
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);

        // Run the rendering loop until the user has attempted to close
        // the window or has pressed the ESCAPE key.
        while ( !glfwWindowShouldClose(window) ) {
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the framebuffer

            glfwSwapBuffers(window); // swap the color buffers

            // Poll for window events. The key callback above will only be
            // invoked during this call.
            glfwPollEvents();
        }
    }

    public static void main(String[] args) {
        new HelloWorld().run();
    }

}

最後にビルドして起動すると以下のような真っ赤なウィンドウが表示される。

$ ./gradlew build run

f:id:narinymous:20180404014703p:plain

エラーが起きる場合

Gradleのrunタスクでアプリケーションを実行する際に以下のような例外が吐かれて躓いたので記しておく。

> Task :run
Exception in thread "main" java.lang.ExceptionInInitializerError
        at org.lwjgl.glfw.GLFW.glfwCreateWindow(GLFW.java:1823)
        at App.init(App.java:38)
        at App.run(App.java:18)
        at App.main(App.java:81)
Caused by: java.lang.IllegalStateException: GLFW windows may only be created on the main thread and that thread must be the first thread in the process. Please run the JVM with -XstartOnFirstThread. For offscreen rendering, make sure another window toolkit (e.g. AWT or JavaFX) is initialized before GLFW.
        at org.lwjgl.glfw.EventLoop$OffScreen.<clinit>(EventLoop.java:39)
        ... 4 more

これはbuild.gradleに書いている以下の行を書くことで回避できる

applicationDefaultJvmArgs = ["-XstartOnFirstThread"]

原因はおそらくgradleのapplicationプラグインを使っていることだと思うが、実装したAppクラスがメインスレッドとして実行されていないのでJVM引数に渡してあげる必要があるようだ。

原因がわかる方は是非教えていただきたい。

TypeScript2.8のConditional typesについて

先日(2018-03-27)TypeScript 2.8が正式リリースされました。
今回のリリースで搭載されたConditional typesという条件付きの型宣言構文についてまとめる。

Conditional typesについて

A extends B ? C : D

構文は上記の通りで、型Aが型Bに割当て可能ならC、それ以外はDになる。
簡単な使用例が以下。

interface Animal {
    live(): void;
}
interface Dog extends Animal {
    woof(): void;
}

// Has type 'number'
type Foo = Dog extends Animal ? number : string;

// Has type 'string'
type Bar = RegExp extends Dog ? number : string;

この簡単な例ではConditional typesによりFooはnumberになり、Barはstringになる。
しかしConditional typesなんて使わずに直にnumber, stringで型宣言してしまっても特に問題ない場面の方が多い気がする。
どんな時に使用するのかいまいち思いつかないが、ドキュメントにはジェネリクスと共に使用するとメリットがわかるとのこと。
ジェネリクスと共に使用したのが以下。

interface Id { id: number, /* other fields */ }
interface Name { name: string, /* other fields */ }

declare function createLabel(id: number): Id;
declare function createLabel(name: string): Name;
declare function createLabel(name: string | number): Id | Name;


type IdOrName<T extends number | string> =
    T extends number ? Id : Name;

declare function createLabel<T extends number | string>(idOrName: T):
    T extends number ? Id : Name;

let a = createLabel("typescript");   // Name
let b = createLabel(2.8);            // Id
let c = createLabel("" as any);      // Id | Name
let d = createLabel("" as never);    // never

createLabel関数は入力の型に基づいて、一つのcreateLabel関数を選択する。
これによりそれぞれ再代入時に許容される型はaはName型、bはId型、cはId型とName型両方、dはnever型となる。

他にも以下のような使い方があるようだ。

type Common<T, U> = T extends U ? never : T;  // Remove types from T that are assignable to U
type Diff<T, U> = Pick<T, Common<keyof T, keyof U>>;

interface UserA {
  name: string,
  age: number,
  email: string,
  phone: string,
}
interface UserB {
  name: string,
  age: number,
}

type CommonProperties = Common<UserB, UserA>; // inferred as { one: string; two: number; }
type DiffProperties = Diff<UserA, UserB>; // inferred as { one: string; two: number; }

const commonUser1: CommonProperties = {
  name: "Hoge",
  age: 23,
}

const commonUser2: CommonProperties = {
  name: "Hoge",
  age: 23,
  email: "example@example.com",
  phone: "xxx-xxxx-xxxx"
} 
//=> error TS2322: Type '{ name: string; age: number; email: string; phone: string; }' is not assignable to type 'UserB'.
//                 Object literal may only specify known properties, and 'email' does not exist in type 'UserB'.

const exeptUser1: DiffProperties = {
  email: "example@example.com",
  phone: "xxx-xxxx-xxxx"
}

const exeptUser2: DiffProperties= {
  name: "Hoge",
  age: 23,
  email: "example@example.com",
  phone: "xxx-xxxx-xxxx"
}
//=> error TS2322: Type '{ name: string; age: number; email: string; phone: string; }' is not assignable to type 'Pick<UserA, "email" | "phone">'.
//                 Object literal may only specify known properties, and 'name' does not exist in type 'Pick<UserA, "email" | "phone">'.

2つのObjectの部分集合を型としたい時や、その逆(部分集合とはならない部分)を型にしたい時とか。
このあたりは使う機会もありそう。

TornadoFXでデスクトップアプリ開発 <実装編>

導入編に続いて、今回はTornadoFXでUIと内部ロジックの実装をしていきます。
JavaFXにはUIを記述する方法がいくつかあります。

  • FXMLというマークアップで記述し実装側でレイアウト系のクラスを継承したクラスと紐づける
  • コードでcontrol系のインスタンス(ボタン、ラベル、テキスト)を生成しレイアウト系のインスタンスに割り当てていく

今回は前者のFXML形式を使っていきます。
JavaFXとは言いましたが、TornadoFXはJavaFXの拡張なので実装の違いは特にありません。
ではまずUIから実装していきます。

1. UIツールセットの導入

今回はマテリアルデザインを簡単にしてくれるJFoenixというライブラリを使用しましょう。
build.gradleのdependenciesに以下の1行を追加します。
 

build.gradle

dependencies {
    implementation "com.google.guava:guava:21.0"
    testImplementation "junit:junit:4.12"
    compile "no.tornado:tornadofx:1.7.14"

    // これを追加
    compile 'com.jfoenix:jfoenix:1.10.0'
}

2. UIの実装

まず完成後の実装を載せます。
 

src/main/resources/Calculator.fxml

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.HBox?>
<?import java.net.URL?>
<?import com.jfoenix.controls.JFXButton ?>

<VBox spacing="10" xmlns="http://javafx.com/javafx/null" xmlns:fx="http://javafx.com/fxml/1">
  <stylesheets>
    <URL value="@/css/application.css"/>
  </stylesheets>
  <padding>
    <Insets top="30" right="30" bottom="30" left="30"/>
  </padding>

  <GridPane alignment="center" hgap="5" vgap="5">
    <HBox alignment="top_right" styleClass='result'
      GridPane.rowIndex="0" 
      GridPane.columnIndex="0"
      GridPane.columnSpan="4">
      <Label fx:id="count" style="-fx-text-fill: white"/>
    </HBox>

    <JFXButton GridPane.columnIndex="0" GridPane.rowIndex="1" text="C"   onAction="#onClearClick"     styleClass="symbol"/>
    <JFXButton GridPane.columnIndex="1" GridPane.rowIndex="1" text="+/-" styleClass="symbol"/>
    <JFXButton GridPane.columnIndex="2" GridPane.rowIndex="1" text="%"  styleClass="symbol"/>
    <JFXButton GridPane.columnIndex="3" GridPane.rowIndex="1" text="÷"   onAction="#onOperationClick" styleClass="symbol"/>

    <JFXButton GridPane.columnIndex="0" GridPane.rowIndex="2" text="7" onAction="#onNumberClick"     styleClass="number"/>
    <JFXButton GridPane.columnIndex="1" GridPane.rowIndex="2" text="8" onAction="#onNumberClick"     styleClass="number"/>
    <JFXButton GridPane.columnIndex="2" GridPane.rowIndex="2" text="9" onAction="#onNumberClick"     styleClass="number"/>
    <JFXButton GridPane.columnIndex="3" GridPane.rowIndex="2" text="x" onAction="#onOperationClick"  styleClass="symbol"/>

    <JFXButton GridPane.columnIndex="0" GridPane.rowIndex="3" text="4" onAction="#onNumberClick"      styleClass="number"/>
    <JFXButton GridPane.columnIndex="1" GridPane.rowIndex="3" text="5" onAction="#onNumberClick"      styleClass="number"/>
    <JFXButton GridPane.columnIndex="2" GridPane.rowIndex="3" text="6" onAction="#onNumberClick"      styleClass="number"/>
    <JFXButton GridPane.columnIndex="3" GridPane.rowIndex="3" text="-" onAction="#onOperationClick"   styleClass="symbol"/>

    <JFXButton GridPane.columnIndex="0" GridPane.rowIndex="4" text="1" onAction="#onNumberClick"      styleClass="number"/>
    <JFXButton GridPane.columnIndex="1" GridPane.rowIndex="4" text="2" onAction="#onNumberClick"      styleClass="number"/>
    <JFXButton GridPane.columnIndex="2" GridPane.rowIndex="4" text="3" onAction="#onNumberClick"      styleClass="number"/>
    <JFXButton GridPane.columnIndex="3" GridPane.rowIndex="4" text="+" onAction="#onOperationClick"   styleClass="symbol"/>

    <JFXButton GridPane.columnIndex="0" GridPane.rowIndex="5" text="0" onAction="#onNumberClick"    styleClass="number"/>
    <JFXButton GridPane.columnIndex="2" GridPane.rowIndex="5" text="." styleClass="symbol"/>
    <JFXButton GridPane.columnIndex="3" GridPane.rowIndex="5" text="=" onAction="#onCalculateClick" styleClass="symbol"/>
  </GridPane>
</VBox>

各レイアウトについては特に難しい概念がないので省きます。
JFXButtonに対してonAction属性を付与していますが、これはロジックの実装の際にController側で実装するハンドラーメソッドです。
各ボタンが押されたら、Controller側の任意のメソッドが実行されます。

導入編ではgraldeプロジェクトで初期化したので、このfxmlはsrc/main/resources直下に置きます。
mavenを使っている人も同じだと思います。

一点注意したいのは以下の一部抜粋した箇所です。

  <stylesheets>
    <URL value="@/css/application.css"/>
  </stylesheets>

今回スタイルも記述したので、application.cssというファイルをsrc/main/resources/css直下に配置し、以下のスタイルを記述します。
 

src/main/resources/css/application.css

.root {
  -fx-background-color: #111;
}

.jfx-button {
  -fx-text-fill: WHITE;
  -fx-min-width: 45px;
  -fx-min-height: 45px;
}

.result {
  -fx-text-fill: WHITE;
  -fx-font-size: 25px;
}

.number {
  -fx-background-color: #00d11e;
  -fx-font-size:15px;
}

.symbol {
  -fx-background-color: #ff8421;
  -fx-font-size:15px;
}

.rootはHTMLでいうところのbodyだと思ってください。
各レイアウト要素自体へのスタイル割り当てはほぼHTML, CSSと同様ですが、JavaFXの場合は.要素名で任意の要素を使用する全ての箇所へのスタイル割り当てが可能です。

例) 全てのLabel要素へテキストカラーを白くするスタイルを割り当てる

.label {
  -fx-text-fill: black;
}

3. ロジック側の実装

UIが完成したので次はUIと紐づくアクション(イベントハンドラー)の実装をします。
まず、前回実装したメインファイルとなるMain.ktを書き換えます。
 

src/main/kotlin/calculator/Main.kt

package calculator
import calculator.controller.CalculatorController
import tornadofx.App

class Main : App(CalculatorController::class)

CalculatorControllerというクラスをimportし、アプリケーション起動時に最初に実行されるControllerとします。
ControllerはFXMLと紐づくため、最初に表示されるのはCalculatorControllerと紐づけようとしている先ほど記述したFXMLです。
では次にCalculatorControllerを実装します。
 

src/main/kotlin/calculator/controller/CalculatorController.kt

package calculator.controller
import  calculator.Operation

import com.jfoenix.controls.JFXButton
import javafx.event.ActionEvent
import javafx.scene.control.Label
import javafx.scene.layout.VBox
import tornadofx.View

class CalculatorController: View() {
  var result: Int =  0
  var refresh: Boolean = false
  var keepingNumber: Int = 0
  var operation: String = ""
  val count: Label by fxid()
  override val root: VBox by fxml("/Calculator.fxml")

  init {
    count.text = ""
    with(root) {
      root.prefWidth = 250.0
      root.prefHeight = 400.0
    }
  }

  /* 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 */
  fun onNumberClick(e: ActionEvent) {
    if (refresh) {
      count.text = ""
      refresh = false
    }

    val text = e.getSource()
    if (text is JFXButton) {
      count.text += text.getText()
      keepingNumber = count.text.toInt()
    }
  }

  /* +, -, x, ÷  */
  fun onOperationClick(e: ActionEvent) {
    if (count.text.isNullOrEmpty()) return
    result = count.text.toInt()
    refresh = false

    count.text = ""
    val text = e.getSource()
    if (text is JFXButton) {
      operation = text.getText()
    }
  }

  fun onCalculateClick() {
    if (count.text.isNullOrEmpty()) return
    when (operation) {
      Operation.plus -> {
        if (refresh) {
          result += keepingNumber
        } else {
          result += count.text.toInt()
        }
      }
      Operation.minus -> {
        if (refresh) {
          result -= keepingNumber
        } else {
          result -= count.text.toInt()
        }
      }
      Operation.multiple -> {
        if (refresh) {
          result *= keepingNumber
        } else {
          result *= count.text.toInt()
        }
      }
      Operation.divide -> {
        if (refresh) {
          result /= keepingNumber
        } else {
          result /= count.text.toInt()
        }
      }
    }
    count.text = result.toString()
    refresh = true
  }

  fun onClearClick() {
    count.text = ""
    result = 0
  }
}

この実装内にはUIの実装で記述したFXMLのonAction属性のvalueに紐づくメソッドが複数あります。
前述した通り、FXMLのonAction属性のvalue値のイベントハンドラーとして実行されるメソッドです。
これを実装したら、最後に実行してみると下図のようにデスクトップアプリが起動するはずです。

f:id:narinymous:20180326021308p:plain

今回は簡素化のため、整数の四則演算のみしか実装していません。
(小数点、剰余、符号反転のボタンは押せるけどなにも動作しない)
もし興味を持って頂けた方は僕のgithubからリポジトリをcloneして追加実装してみてください。

github.com