これは「TeX & LaTeX Advent Caleandar 2017」の25日目の記事です。
(24日目は golden_lucky さん です。)
例によって前フリ
アドベントカレンダーもいよいよ今日が最終日ですが、何だか超絶アレなタイトルです。「システムプログラミング」って何なのでしょうか。チョットこちらの記事を見てみましょう。
- Goで覗くシステムプログラミングの世界(プログラミング+)
本連載では、一番最後の「OSの提供する機能を使ったプログラミング」をシステムプログラミングの定義として話をすすめます。
なるほど。そういうわけで、本記事では、**「TeXで、OSの機能を直接利用する」**という超絶アレな世界をチョット覗いてみます。ただし、対象とするOSはWindowsに限定します。つまり、TeXでWindows APIを呼び出す、という話です。
※本記事で扱ったソースコードは以下のリポジトリに収録されています。
TeX言語でWindows APIの呼出ができる?
**できません。**TeX言語はTeXのプリミティブしか呼び出せません。
アレ、じゃあ記事が成立しない……、いえ、心配ありません。この記事は「TeX言語で覗く~」ではなくて「TeXで覗く~」です。なので、LuaTeXでLua言語を使うことにすればよいのです(強弁)。
LuaTeXでWindows APIの呼出ができる?
Luaでは、外部の(機械語の)実行ファイルを起動したり、機械語の拡張モジュールを読み込むことができます。過去(2012年)のアドベントカレンダーにこんなネタがありましたね1。
- TeX で Windows をシャットダウンする件について(マクロツイーター)
ここで使われている拡張モジュール(shutwindown.dll)の実装では実際に「WindowsをシャットダウンするAPI」(ExitWindowsEx
)を呼び出しています。
えっ、「そのshutwindown.dllはTeXで実装したのでもLuaで実装したのでもない(実際、C言語による実装です)ので、『LuaTeXでWindows APIを呼び出している』とは到底いえない」、ですか。確かにそうですね……。
LuaTeXでWindows APIの呼出ができる!
それはそうとして、このLuaLaTeX文書を見てください。
% LuaLaTeX document
\documentclass{article}
\begin{document}
\directlua{
local ffi = require("ffi")
ffi.cdef [[
int MessageBoxA(void *hWnd, const char *lpText,
const char *lpCaption, unsigned uType);
]]
ffi.C.MessageBoxA(nil, "Hello LuaTeX world!", "Test", 0)
}
\end{document}
- ソース:hello.tex
これを、-shell-escape
付きでlualatex
でコンパイルします。
※プロンプトを>
で表します。
>lualatex -shell-escape hello
おおっ、なんと、TeXなのにシステムのダイアログが出現しました。スゴイ!
何が起こったのでしょう。-shell-escape
付きなので、何か外部コマンドを実行した? でも\directlua
の中のLuaコードをみてもコマンド行らしきものは見当たりません。代わりに、何やらC言語のようなコードがあって、そこにAPI名MessageBoxA
が書いてあります。
FFIライブラリ、スゴイ
これは、[FFI]という仕組を利用しています。LuaFFIは元々は[LuaJIT]2で用意されているライブラリで、LuaからのCの関数を呼び出す際の手続きを簡略化するためのものです。
通常、Luaから呼び出せるCの関数は「Luaの関数の実装であるもの」に限られます。従って、例えばWindows APIのMessageBoxA
を呼び出すためには、あらかじめ「MessageBoxA
を呼び出す『Luaの関数の実装』となるCの関数」を実装する必要があります。ところが、FFIの仕組を用いると、“橋渡し”のためのCの実装が不要になり、単に「関数の宣言のコード」をLuaコード中で指定するだけでCの関数が直接呼び出せるようになるのです。
本記事では、FFIの仕組を利用して、LuaTeXエンジン3からWindows APIを呼び出す事例を紹介します。ただし以降では、LuaTeXのTeXエンジン(組版エンジン)としての実行ではなくLuaインタプリタとしての実行([いわゆる“texlua”])に焦点を当てます。
その理由は「FFIによるWindows API呼出」という手法について、“texlua”での利用についてより実用性があると考えているからです。TeX Liveに含まれるTeX関連の補助ツールは“texlua”で実装されています。TeX関連ツールはクロスプラットフォームである必要がありますが、LuaTeXの機能だと「Windowsだけ機能が足りない」という事例が時々生じます。「Windows API呼出」はそういう場合の解決になりえる手法だと考えているのです。
前提知識
- フツーのLuaの知識。
- フツーのC言語の知識。
- ほんのチョットのWindows APIの知識(大丈夫です、私もほとんど知りません4)。
- TeX言語の知識は全然サッパリ全く不要。
- LaTeXの知識もほぼ不要(えっ)
はじめてのFFI
まずは単純な呼出規約をもつWindows APIを利用して、FFIライブラリの使い方を説明していきます。
事例①:LuaだってプロセスIDしたい
PerlやRubyでは$$
で現在プロセスのIDを取得できます。これをLuaTeXでやってみましょう。
local ffi = require("ffi")
-- 使用するAPIの宣言を行う
ffi.cdef[[//C言語(C99)ソース文字列
//// 型宣言(typedef等)
typedef unsigned long DWORD;
//// プロトタイプ宣言
//現在プロセスのプロセスIDを取得
DWORD GetCurrentProcessId(void); //戻り値:プロセスID
]]
-------- 使用例
-- 宣言したAPI関数を呼び出す
local pid = ffi.C.GetCurrentProcessId()
-- pidにはプロセスIDの数値が返る
print("Process Id = "..tostring(pid))
-- EOF
- ソース:pid.lua
このLuaプログラムを“texlua”で実行すると、現在プロセスIDが出力されます。
>texlua pid.lua
Process Id = 9924
プロセスIDを得るAPI
ここでは[GetCurrentProcessId]というAPIを利用しています。このAPIの関数プロトタイプは次の通りです5。
DWORD GetCurrentProcessId(void);
Windowsプログラミングでは基本的な型(数値型、ポインタ型)について、独特な別名(typedef)を使う習慣があり、DWORD
もその一つです。DWORD
型の定義は以下の通りです。
typedef unsigned long DWORD;
つまり、普通にC言語のプログラムを書く場合、GetCurrentProcessId
のAPIを使うためには上記の2行を前もって宣言する必要があるわけです。(もちろんこの記述はヘッダファイルとして提供されているのでそれを読み込むのが通例でしょう。)
FFIの利用の手順
LuaTeXではFFIライブラリはffi
という名前のモジュールとしてプリロード6されているので、これをロードします。
local ffi = require("ffi")
そして、先述の「GetCurrentProcessId
を使うために必要なC言語の宣言のコード」の文字列を引数としてFFIの関数ffi.cdef()
を実行します。これで実際にGetCurrentProcessId
を使う準備が行われます。(つまり、FFIはC言語の“宣言”だけを解釈するパーザを備えているわけです。)
ffi.cdef[[//この中にC言語(C99)の"宣言"を書く
typedef unsigned long DWORD;
DWORD GetCurrentProcessId(void);
]]
kernel32およびuser32に属するAPI(GetCurrentProcessId
も該当する)はffi.C
という名前空間(テーブル)の下に配置されます。従って、あとはffi.C.GetCurrentProcessId()
を普通のLua関数のように呼び出せば、Windows APIのGetCurrentProcessId
の呼出が行われます。
local pid = ffi.C.GetCurrentProcessId()
ここで、GetCurrentProcessId
APIの返した値が(DWORD
の)9924だったとすると、Luaのffi.C.GetCurrentProcessId()
の戻り値は「Luaの数値型7の9924」になります。このようにC言語のデータ型とLuaのデータ型の間は可能な限り“自然な形で”自動的に変換が行われます。従って、この後は変数pid
が「C言語の関数から返った値」であることを意識する必要はありません。
事例②:五秒間待ってやる
Luaにはいわゆる“sleep”の機能がありません。これをWindows APIのSleep
で実現してみましょう。
local ffi = require("ffi")
ffi.cdef[[
//// 型宣言(typedef等)
typedef unsigned long DWORD;
//// プロトタイプ宣言
//指定時間だけプロセス実行を中断
void Sleep(
DWORD dwMilliseconds); //入力:待機時間(ミリ秒)
]]
-------- 使用例
for count = 5, 1, -1 do
print(count)
ffi.C.Sleep(1000) -- 1秒待つ
end
print("Finish!")
-- EOF
- ソース:sleep.lua
実行すると、5から始まるカウントダウンが1秒ごとに表示されます。
>texlua sleep.lua
5
4
3
2
1
Finish!
引数をもつAPIを扱う
Sleep
の関数プロトタイプは次の通りです。
void Sleep(DWORD dwMilliseconds);
必要な宣言のコードを記した文字列を引数にしてffi.cdef()
を呼ぶと、ffi.C.Sleep()
が配置されます。ここまで先の例と同じです。ここで、「1秒待機する」には、Sleep
APIのDWORD
型の引数に1000を指定する必要がありますが、どうすればよいでしょうか。
先の例では関数の戻り値について「データ型は“自然に”自動変換される」ことを見ましたが、これは引数についても同様です。つまり、「Luaの数値1000」を与えれば、それが自動的にDWORD
型に変換されます。簡単ですね。
ffi.C.Sleep(1000)
事例③:とあるOSの短形式名(ショートネーム)
Luaは現代のスクリプト言語の中では珍しく、言語自体ではUnicode文字を扱わず、文字列を常にバイト列として扱います。この性質のため、Windowsでの非ASCIIのファイル名の取扱は非常に厄介です。
ソレの名は。
※以下、ANSIコードページは932(日本語)であると仮定します。
WindowsのGUI操作ではファイル名に任意のUnicode文字を含めることができるため、コードページ932(CP932)にない文字のファイルは普通に存在しえます。例えば、ディレクトリC:\tmp\temp1
に次の名前のファイルがあったとします。
-
test file.txt
(ASCII文字のみ) -
test ☆彡.txt
(非ASCIIだがCP932にある文字を含む) -
test ☃♪.txt
(非ASCIIでCP932にない文字☃
を含む)
ファイル名を引数にとるLuaの関数を使った場合、「Luaの文字列はバイト列」という性質上8、ANSI版のAPIが使われることになります。従って、もしLuaスクリプトをCP932で書いているのであれば、非ASCIIのファイル名も期待通り取り扱えます。しかしこの場合、CP932にない文字のファイル名test ☃♪.txt
はスクリプト中に書くことすらできません。
--文字コードはCP932
lfs = require("lfs")
-- lfs.attributes()でファイルサイズを調べる
print((lfs.attributes("test file.txt", "size")))-->42
print((lfs.attributes("test ☆彡.txt", "size")))-->56
--3つ目のファイルの名前は書けない!
クロスプラットフォームでLuaスクリプトを作る場合は、スクリプト本体も、それに対する入力データ(を記したファイルなど)もUTF-8で書かれるでしょう。その場合は、非ASCII文字を含むファイルは(バイト列が食い違うため)全く見つけられなくなります。
--文字コードはUTF-8
lfs = require("lfs")
print((lfs.attributes("test file.txt", "size")))-->42
print((lfs.attributes("test ☆彡.txt", "size")))-->nil (見つからない)
print((lfs.attributes("test ☃♪.txt", "size")))-->nil (見つからない)
要するに、CP932外の文字を含むtest ☃♪.txt
のようなファイル名はLuaの機能ではどうやっても扱えない、という困った事態になっているわけです。
※ちなみに、RubyやPythonはCP932外のファイル名を扱うことができます。Perlはアレ(?)なようです。
困ったときのショートネーム
この問題の(チョットアレな)回避策として、ショートネームを使うというものがあります。ショートネームはただ短いだけでなく、「ANSIコードページにある文字のみからなる」という性質があるからです。
-
test file.txt
→TESTFI~1.TXT
-
test ☆彡.txt
→TEST☆~1.TXT
-
test ☃♪.txt
→TEST♪~1.TXT
(CP932にない☃
は入らない)
従って、ショートネームのTEST♪~1.TXT
であれば、Luaの関数(ANSI版のAPIに帰着する)で扱えないという問題は避けられそうです9。
前置きが長くなりましたが、ここではWindows APIを使って、ファイルのショートネームを取得してみます。
実際にショートネームしてみた
ファイルのショートネームを取得するAPIは、それ自身、ANSI版GetShortPathNameA
とUnicode版GetShortPathNameW
があります10。先述の通り、Luaの文字列はバイト列なので、GetShortPathNameA
を使う方が理に適ってそうです。
local ffi = require("ffi")
ffi.cdef[[
//// 型宣言
typedef unsigned long DWORD;
typedef const char *LPCSTR;
typedef char *LPSTR;
//// プロトタイプ宣言
//短形式パス名を取得
DWORD GetShortPathNameA( //戻り値:出力パス名のサイズ,0=失敗
LPCSTR lpszLongPath, //入力:長形式パス名
LPSTR lpszShortPath, //出力:短形式パス名
DWORD cchBuffer); //出力:lpszShortPathのサイズ
]]
--- 短形式パス名を取得.
-- @param lpath 長形式パス名
-- @return 短形式パス名
function short_path_name(lpath)
local spath = ffi.new("char[?]", 512)
local splen = ffi.C.GetShortPathNameA(lpath, spath, 512)
if splen == 0 then return nil end -- 失敗
return ffi.string(spath, splen)
end
-------- 使用例
print(short_path_name("test file.txt"))
-- EOF
- ソース:shortname-1.lua
先ほどのC:\tmp\temp1
ディレクトリで実行すると、正しいショートネームが表示されました。
>texlua shortname-1.lua
TESTFI~1.TXT
バイト文字列の扱い
GetShortPathNameA
の関数プロトタイプは次のようになっています。
DWORD GetShortPathNameA(LPCSTR lpszLongPath,
LPSTR lpszShortPath, DWORD cchBuffer);
この中のLPCSTR
とLPSTR
はchar
のポインタの型であり、バイト文字列(CP932の場合は“マルチバイト文字列”)を表しています。このAPIに対応するLua関数ffi.C.GetShortPathNameA()
の引数はどうすればよいでしょうか?
入力用の引数lpszLongPath
(対象のロングファイルネーム)の方は簡単で、「Luaの文字列lpath
」を与えれば済みます。これで、Cの関数では当該のバイト列が置かれたメモリ領域の先頭のポインタが渡されます11。
これに対して出力用の引数lpszShortPath
(結果のショートファイルネーム)については、内容が可変となるバイトのバッファを与える必要があります。つまり「普通のLuaの値」ではなく「FFI専用の特殊な値」が要るわけです。そのような特殊な値を生成するためのFFIの機能がffi.new()
関数です。
local spath = ffi.new("char[?]", 512)
これで、512要素のchar
の“Cの配列”(512バイト)が確保12され、その配列の先頭のポインタ(char *
型)を表すユーザデータ13が変数spath
に代入されます。このspath
をlpszShortPath
引数(char *
型)に渡せばよいわけです。
ffi.C.GetShortPathNameA()
の実行が正常に完了すると、配列spath
には結果のショートネームが格納された状態になっています。“Cの配列”はLuaで値が読み出せる14ので、ここから「Luaの文字列」を作り出すのは難しくありません。FFIではこの機能がライブラリ関数ffi.string()
として用意されています。実際の使われ方を見ればその仕様は明らかでしょう15。
-- 配列spathの先頭のsplenバイトからなるLua文字列
return ffi.string(spath, splen)
これで一件落着…しない
ところで、この節の最初で、ショートネームを取得する目的は「非ASCIIのファイル名を扱うため」としました。この目的は果たせているでしょうか? 実装したshort_path_name()
を非ASCIIのファイル名に適用してみましょう。
--文字コードはUTF-8
print(short_path_name("test ☆彡.txt"))-->nil
print(short_path_name("test ☃♪.txt"))-->nil
-- EOF
うまくいきません! よく考えてみると、「ANSI版のAPIが使われるから失敗する」という理屈なのに、独自にGetShortPathName
のAPIを呼ぶときにANSI版を使ったのでは何の回避にもなっていません16。ここはUnicode版のGetShortPathNameW
を使うべきでした。でもUTF-8バイト列しか扱えないLuaでどうやって「Unicode文字列(ワイド文字列)」を作ればいいのでしょうか。やはり文字コード関連のAPIも検討しないといけないようです。
もっともっとFFI
なんとなくFFIのキホンがつかめてきました。ここからはホンキで「FFIでシステムプログラミング」に取り組みましょう。
事例④:UTF-8は正義
クロスプラットフォームのためUTF-8で書かれたスクリプトでは、当然文字列はUTF-8のバイト列となります。これをそのままprint()
等で出力するとWindowsでは文字化けしてしまうため、Windows用に端末出力コードページ(日本語Windowsだと通常こちらもCP932)に文字コード変換する関数を作成します。
※「Windows以外では端末はUTF-8に決まってるでしょ」という前提です。
-- 文字コードはUTF-8
local ffi = require("ffi")
require("ffi_typedef")
ffi.cdef[[
//// プロトタイプ宣言
//端末出力コードページを取得
UINT GetConsoleOutputCP(void); //戻り値:端末出力コードページ
//多バイト文字列をワイド文字列に変換
int MultiByteToWideChar( //戻り値:変換後文字列の長さ,0=失敗
UINT CodePage, //入力:コードページ
DWORD dwFlags, //入力:文字種別フラグ
LPCSTR lpMultiByteStr, //入力:変換元文字列
int cbMultiByte, //入力:変換元文字列の長さ
LPWSTR lpWideCharStr, //出力:変換後文字列
int cchWideChar); //出力:lpWideCharStrのサイズ
//ワイド文字列を多バイト文字列に変換
int WideCharToMultiByte( //戻り値:変換後文字列の長さ,0=失敗
UINT CodePage, //入力:コードページ
DWORD dwFlags, //入力:文字種別フラグ
LPCWSTR lpWideCharStr, //入力:変換元文字列
int cchWideChar, //入力:変換元文字列の長さ
LPSTR lpMultiByteStr, //出力:変換後文字列
int cbMultiByte, //出力:lpMultiByteStrのサイズ
LPCSTR lpDefaultChar, //入力:代替文字
LPBOOL lpUsedDefaultChar); // 出力:代替文字が使われたか
]]
Ccst = { -- 定数定義
CP_UTF8 = 65001; -- UTF-8のコードページ
}
--- 文字列を端末出力コードページに変換
-- @param str UTF-8文字列
-- @return 端末出力CP文字列(nil=失敗)
function costr(str)
local istr = tostring(str)
if #istr == 0 or ffi.os ~= "Windows" then return istr end
-- UTF-8文字列→ワイド文字列
local ustr = ffi.new("WCHAR[?]", #istr)
local ulen = ffi.C.MultiByteToWideChar(Ccst.CP_UTF8, 0,
istr, #istr, ustr, #istr)
if ulen == 0 then return nil end -- 失敗
-- ワイド文字列→端末出力文字列
local cocp = ffi.C.GetConsoleOutputCP()
local ostr = ffi.new("char[?]", #istr)
local olen = ffi.C.WideCharToMultiByte(cocp, 0,
ustr, ulen, ostr, #istr, nil, nil)
if ulen == 0 then return nil end -- 失敗
return ffi.string(ostr, olen)
end
-------- 使用例
print(costr("TeX言語危険、ダメゼッタイ!"))
-- EOF
- ソース:console-1.lua
実行結果は以下の通りです。スクリプト中でUTF-8で与えた文字列が化けずに正常に出力されています。
>texlua console-1.lua
TeX言語危険、ダメゼッタイ!
解説してみる
次のような方針をとっています。
-
MultiByteToWideChar
を使って、UTF-8(コードページ65001)のマルチバイト文字列をUnicode文字列(ワイド文字列)に変換。 -
WideCharToMultiByte
を使って、Unicode文字列を端末出力コードページ(これはGetConsoleOutputCP
で取得する)に変換。
前段の変換では「ワイド文字(WCHAR
型)の配列」を用意する必要がありますが、これは先と同様にffi.new()
で作成できます。
local ustr = ffi.new("WCHAR[?]", #istr)
注意事項
- 「Windows特有のtypedef」を都度書くのは面倒なので、当該のtypedef全部を定義するためのモジュール
ffi_typedef
を作りました(ソース:ffi_typedef.lua)。取りあえず、カレントディレクトリに置いておけばよいでしょう。 -
Ccst
はFFIとは無関係の単なるテーブルです。Windowsプログラミングで使われる定数を収めています17。 - Unicode文字列の長さ18はそれを表すUTF-8のバイト列の長さを超えないので、
ustr
の要素数はistr
の長さとしています。 - UTF-8よりもバイト列が長くなる文字コードはなさそうなので、
ostr
の要素数も同様にしています。 -
ffi.C.WideCharToMultiByte()
の最後の2引数にnil
を渡していますが、これはC言語側ではNULLポインタと解釈されます。 -
ffi.os
はプラットフォームの種別を表す文字列です。これがWindows
でない場合は入力バイト列を変換せずにそのまま返すことにすれば、「Windows以外ではUTF-8でそのまま出力」することになります。
蛇足事項
入力の文字列に端末出力コードページ(CP932)にない文字が含まれる場合、この実装では当然そういう文字は正常に出力できません(代替文字?
に化けます)。端末出力の部分をLuaの機能(print()
)で行う限りはこれは避けられません。ところが、端末出力自体をWindowsのAPIで行うと任意のUnicode文字が出力できます。参考としてこれの実装例を挙げておきます。
- ソース:console-2.lua
事例⑤:メイク・ファイル名・ショート・アゲイン
文字コード変換とワイド文字列の扱いが判ったので、もう一度、「ファイルのショートネームを取得する」ことに挑戦してみます。
-- 文字コードはUTF-8
local ffi = require("ffi")
require("ffi_typedef")
ffi.cdef[[
//// プロトタイプ宣言
//短形式パス名を取得(Unicode版)
DWORD GetShortPathNameW( //戻り値:出力パス名のサイズ,0=失敗
LPCWSTR lpszLongPath, //入力:長形式パス名
LPWSTR lpszShortPath, //出力:短形式パス名
DWORD cchBuffer); //出力:lpszShortPathのサイズ
//ANSIコードページを取得
UINT GetACP(void); //戻り値:ANSIコードページ
//多バイト文字列をワイド文字列に変換
int MultiByteToWideChar( //戻り値:変換後文字列の長さ,0=失敗
UINT CodePage, //入力:コードページ
DWORD dwFlags, //入力:文字種別フラグ
LPCSTR lpMultiByteStr, //入力:変換元文字列
int cbMultiByte, //入力:変換元文字列の長さ
LPWSTR lpWideCharStr, //出力:変換後文字列
int cchWideChar); //出力:lpWideCharStrのサイズ
//ワイド文字列を多バイト文字列に変換
int WideCharToMultiByte( //戻り値:変換後文字列の長さ,0=失敗
UINT CodePage, //入力:コードページ
DWORD dwFlags, //入力:文字種別フラグ
LPCWSTR lpWideCharStr, //入力:変換元文字列
int cchWideChar, //入力:変換元文字列の長さ
LPSTR lpMultiByteStr, //出力:変換後文字列
int cbMultiByte, //出力:lpMultiByteStrのサイズ
LPCSTR lpDefaultChar, //入力:代替文字
LPBOOL lpUsedDefaultChar); // 出力:代替文字が使われたか
]]
Ccst = { -- 定数定義
CP_UTF8 = 65001; -- UTF-8のコードページ
}
--- 短形式パス名を取得.
-- @param lpath 長形式パス名
-- @return 短形式パス名(nil=失敗)
function short_path_name(lpath)
if type(lpath) ~= "string" then return nil end
if ffi.os ~= "Windows" then return lpath end
-- UTF-8文字列→ワイド文字列
local ulpath = ffi.new("WCHAR[?]", #lpath + 1)
local ulplen = ffi.C.MultiByteToWideChar(Ccst.CP_UTF8, 0,
lpath, #lpath + 1, ulpath, #lpath + 1)
if ulplen == 0 then return nil end -- 失敗
-- 長形式→短形式の変換
local usplen = ffi.C.GetShortPathNameW(ulpath, nil, 0)
if usplen == 0 then return nil end -- 失敗
local uspath = ffi.new("WCHAR[?]", usplen + 1)
usplen = ffi.C.GetShortPathNameW(ulpath, uspath, usplen + 1)
if usplen == 0 then return nil end -- 失敗
-- ワイド文字列→ANSI文字列
local acp = ffi.C.GetACP()
local spath = ffi.new("char[?]", usplen * 3)
local dflt = ffi.new("BOOL[1]")
local splen = ffi.C.WideCharToMultiByte(acp, 0,
uspath, usplen, spath, usplen * 3, nil, dflt)
if splen == 0 or dflt[0] ~= 0 then return nil end -- 失敗
return ffi.string(spath, splen)
end
-------- 使用例
print(short_path_name("test file.txt"))
print(short_path_name("test ☆彡.txt"))
print(short_path_name("test ☃♪.txt"))
-- EOF
- ソース:shortname-2.lua
先と同じくC:\tmp\temp1
で実行してみます。今度は非ASCIIのファイル名も正常に扱うことができました!
>texlua shortname-2.lua
TESTFI~1.TXT
TEST☆~1.TXT
TEST♪~1.TXT
ショートネームが取得できたので、これを使って実際にLuaでファイルが扱えることを確かめてみます。shortname-2.lua
の末尾に次のコードを追加します。
--- ファイルサイズの情報を表示する.
-- @param no 説明用の番号
-- @param path パス名
function show_file_size(no, path)
local size = lfs.attributes(path, "size")
print("("..no..") "..
((size) and "size = "..size or "not found"))
end
-- テスト
show_file_size("1", "test file.txt")
show_file_size("2", "test ☆彡.txt")
show_file_size("3", "test ☃♪.txt")
show_file_size("4", short_path_name("test file.txt"))
show_file_size("5", short_path_name("test ☆彡.txt"))
show_file_size("6", short_path_name("test ☃♪.txt"))
TESTFI~1.TXT
TEST☆~1.TXT
TEST♪~1.TXT
(1) size = 42
(2) not found
(3) not found
(4) size = 42
(5) size = 56
(6) size = 888
元のファイル名を使った(1)~(3)では非ASCIIのファイル名の時に失敗しますが、一度ショートネームに変換した(4)~(6)では全て成功しています。当初の目的通り、CP932にない文字を含む名前のファイルをLuaで取り扱うことに成功しました。
解説してみる
要するに、事例③と事例④で得た知識を組み合わせて、以下の手順で「CP932のショートネーム」を取得しています。
UTF-8バイト列のロングネーム
↓ (MultiByteToWideChar)
ワイド文字列のロングネーム
↓ (GetShortPathNameW)
ワイド文字列のショートネーム
↓ (WideCharToMultiByte)
ANSI(CP932)バイト列のショートネーム
最後のffi.C.WideCharToMultiByte()
の呼出は少し注意が必要です。ここでは、CP932に変換できない文字があったかどうかを調べる(あった場合は結果の文字列はファイル名として使えないので失敗と見なす)ために、WideCharToMultiByte
APIの引数lpUsedDefaultChar
を利用しています。これはBOOL
19のポインタ渡しの引数であるため、通常のC言語のコードでは次のように書かれるはずです。
BOOL dflt = FALSE;
// 変換不能な文字があった場合、dfltが真に設定される
WideCharToMultiByte(acp, 0, uspath, usplen, spath, usplen * 3,
NULL, &dflt);
しかし、FFIではC言語のアドレス演算子(&
)に相当するものがありません。そこで、代わりに次のようなC言語のコードを想定して、これをFFIに翻訳します。
BOOL dflt[1] = { FALSE }; // dfltはBOOL*型
// 変換不能な文字があった場合、dflt[0]が真に設定される
WideCharToMultiByte(acp, 0, uspath, usplen, spath, usplen * 3,
NULL, dflt);
つまり、BOOL
の1要素の配列をffi.new()
で用意すればよいわけです20。
local dflt = ffi.new("BOOL[1]") -- ゼロクリアされる
-- 変換不能な文字があった場合、dflt[0]が非ゼロになる
local splen = ffi.C.WideCharToMultiByte(acp, 0,
uspath, usplen, spath, usplen * 3, nil, dflt)
その他の注意事項を挙げておきます。
-
GetShortPathNameW
は文字列をナル終端として扱います。このため、前段のMultiByteToWideChar
ではナル文字を結果のワイド文字列に含めるために長さを1増やしています。なお、Lua文字列をchar *
型の値として使う場合、末尾にナル文字があることは保証されます。 -
GetShortPathNameW
を一度cchBuffer
を0として読んでいるのは、ショートネームの長さを取得するためです。 - 「プラットフォームがWindowsでない場合は入力の文字列をそのまま返す」という仕様にしています。
事例⑥:Windowsだってreadlinkしたい
LuaTeX組込のLuaFileSystem(lfs
)モジュールには、機能拡張として「シンボリックリンクの参照先を取得する」ための関数lfs.readlink()
が追加されています。ところがこの関数はWindowsではサポートされていません。そこで、Windowsのデバイス入出力APIを利用して同等の機能のものを実装してみましょう。
例えば、ディレクトリC:\tmp\temp2
に次の名前のファイルがあったとします。
-
original.txt
(リンクでない普通のファイル) -
symlink.txt
(original.txt
へのシンボリックリンク)
実際にreadlinkしてみた
local ffi = require("ffi")
require("ffi_typedef")
ffi.cdef[[
//// 型宣言
typedef void *HANDLE;
typedef struct { //再解析ポイント情報
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
//※VLSとして扱いたいので、不要な共用体要素を省く
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
//可変長配列(VLA)の要素
WCHAR PathBuffer[?];
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
//※以下はダミー定義
typedef void *LPSECURITY_ATTRIBUTES;
typedef void *LPOVERLAPPED;
//// プロトタイプ宣言
//ファイル・ディレクトリの属性を取得する
DWORD GetFileAttributesW( //戻り値:ファイルの属性
LPCWSTR lpFileName); //入力:ファイルパス名
//ファイルを開いてハンドルを作成する
HANDLE CreateFileW( //戻り値:ファイルハンドル
LPCWSTR lpFileName, //入力:ファイルパス名
DWORD dwDesiredAccess, //入力:アクセスモード
DWORD dwShareMode, //入力:共有モード
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //入力:セキュリティ記述子
DWORD dwCreationDisposition, //入力:ファイル作成の指定
DWORD dwFlagsAndAttributes, //入力:ファイル作成時の属性
HANDLE hTemplateFile); //入力:テンプレートファイルハンドル
//デバイスの直接入出力制御
BOOL DeviceIoControl( //戻り値:成功したか
HANDLE hDevice, //入力:デバイスハンドル
DWORD dwIoControlCode, //入力:制御コード
LPVOID lpInBuffer, //入力:入力データ
DWORD nInBufferSize, //入力:lpInBufferのサイズ
LPVOID lpOutBuffer, //出力:出力データのバッファ
DWORD nOutBufferSize, //入力:lpOutBufferのサイズ
LPDWORD lpBytesReturned, //出力:実際の出力データのサイズ
LPOVERLAPPED lpOverlapped); //入力:非同期動作の指定
//オブジェクトハンドルを閉じる
BOOL CloseHandle( //戻り値:成功したか
HANDLE hObject); //入力:オブジェクトハンドル
//多バイト文字列をワイド文字列に変換
int MultiByteToWideChar( //戻り値:変換後文字列の長さ,0=失敗
UINT CodePage, //入力:コードページ
DWORD dwFlags, //入力:文字種別フラグ
LPCSTR lpMultiByteStr, //入力:変換元文字列
int cbMultiByte, //入力:変換元文字列の長さ
LPWSTR lpWideCharStr, //出力:変換後文字列
int cchWideChar); //出力:lpWideCharStrのサイズ
//ワイド文字列を多バイト文字列に変換
int WideCharToMultiByte( //戻り値:変換後文字列の長さ,0=失敗
UINT CodePage, //入力:コードページ
DWORD dwFlags, //入力:文字種別フラグ
LPCWSTR lpWideCharStr, //入力:変換元文字列
int cchWideChar, //入力:変換元文字列の長さ
LPSTR lpMultiByteStr, //出力:変換後文字列
int cbMultiByte, //出力:lpMultiByteStrのサイズ
LPCSTR lpDefaultChar, //入力:代替文字
LPBOOL lpUsedDefaultChar); // 出力:代替文字が使われたか
]]
Ccst = { -- 定数定義
CP_UTF8 = 65001;
FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;
GENERIC_READ = 0x80000000;
FILE_SHARE_READ = 0x00000001;
FILE_SHARE_WRITE = 0x00000002;
FILE_SHARE_DELETE = 0x00000004;
OPEN_EXISTING = 3;
FILE_FLAG_BACKUP_SEMANTICS = 0x2000000;
FILE_FLAG_OPEN_REPARSE_POINT = 0x200000;
INVALID_HANDLE_VALUE = ffi.cast("HANDLE", -1);
FSCTL_GET_REPARSE_POINT = 0x900A8;
IO_REPARSE_TAG_SYMLINK = 0xA000000C;
}
-- cf. http://www.wabiapp.com/WabiSampleSource/windows/get_reparse_point.html
--- シンボリックリンクの参照先を取得する
-- @param path シンボリックリンクのパス名
-- @returns 参照先のパス名(nil=失敗)
function readlink(path)
if type(path) ~= "string" then return nil end
if ffi.os ~= "Windows" then return lfs.readlink(path) end
-- UTF-8文字列→ワイド文字列
local upath = ffi.new("WCHAR[?]", #path + 1)
local uplen = ffi.C.MultiByteToWideChar(Ccst.CP_UTF8, 0,
path, #path + 1, upath, #path + 1)
if uplen == 0 then return nil end -- 失敗
-- ファイルの属性を調べる
local fatr = ffi.C.GetFileAttributesW(upath)
if fatr == -1 or -- APIが失敗
-- ファイルが再解析ポイントでない
not bit32.btest(fatr, Ccst.FILE_ATTRIBUTE_REPARSE_POINT) then
return nil -- 失敗
end
local ret, hfile
repeat -- break可能なブロック
-- ファイルハンドル取得
hfile = ffi.C.CreateFileW(upath, Ccst.GENERIC_READ,
Ccst.FILE_SHARE_READ + Ccst.FILE_SHARE_WRITE + Ccst.FILE_SHARE_DELETE,
nil, Ccst.OPEN_EXISTING,
Ccst.FILE_FLAG_BACKUP_SEMANTICS + Ccst.FILE_FLAG_OPEN_REPARSE_POINT,
nil)
if hfile == Ccst.INVALID_HANDLE_VALUE then break end -- 失敗
-- 再解析ポイントの情報を取得
local uoplen = 32768
local rdata = ffi.new("REPARSE_DATA_BUFFER", uoplen)
local rdlen = ffi.new("DWORD[1]")
local ok = ffi.C.DeviceIoControl(hfile, Ccst.FSCTL_GET_REPARSE_POINT,
nil, 0, rdata, ffi.sizeof(rdata), rdlen, nil)
if ok == 0 then break end -- 失敗
local rtag = bit32.bor(rdata.ReparseTag)
-- 再解析ポイントがシンボリックリンクではない
if rtag ~= Ccst.IO_REPARSE_TAG_SYMLINK then break end -- 失敗
-- SubstituteName~ はバイト単位なので文字単位の値を得る
local sso = rdata.SubstituteNameOffset / 2 -- オフセット
local ssl = rdata.SubstituteNameLength / 2 -- 文字数
-- 文字数が整数でない
if sso % 1 ~= 0 or ssl % 1 ~= 0 then break end -- 失敗
local uopath = rdata.PathBuffer + sso --※ポインタ演算
-- ワイド文字列→ANSI文字列
local opath = ffi.new("char[?]", ssl * 4 + 1)
local oplen = ffi.C.WideCharToMultiByte(Ccst.CP_UTF8, 0,
uopath, ssl, opath, ssl * 4, nil, nil)
if oplen == 0 then break end -- 失敗
ret = ffi.string(opath, oplen)
until true
-- ファイルハンドルを閉じる
if hfile ~= nil then
ffi.C.CloseHandle(hfile)
end
return ret
end
-------- 使用例
print("symlink.txt links to: "..readlink("symlink.txt"));
-- EOF
- ソース:readlink.lua
先ほどのC:\tmp\temp2
ディレクトリで実行した結果です。リンク先のファイル名が正しく取得できています。
>texlua readlink.lua
symlink.txt links to: original.txt
解説してみる
DecideIoControl
で使用している構造体REPARSE_DATA_BUFFER
は、実際には共用体のメンバを含む次のような複雑なものです。
typedef struct { //再解析ポイント情報
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
union { //(共用体)
struct { //シンボリックリンク用
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
WCHAR PathBuffer[1];
} SymbolicLinkReparseBuffer;
struct { //マウントポイント用
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
WCHAR PathBuffer[1];
} MountPointReparseBuffer;
struct { //一般用
UCHAR DataBuffer[1];
} GenericReparseBuffer;
};
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
ffi.cdef()
はこのような複雑な定義にも対応しています。しかし、共用体の部分については、今のプログラムではSymbolicLinkReparseBuffer
しか利用していません。そこで共用体のSymbolicLinkReparseBuffer
の部分を展開(残りのメンバは削除)することにします。
typedef struct { //再解析ポイント情報
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
WCHAR PathBuffer[1]; //可変長配列のメンバ
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
この“簡略版”のREPARSE_DATA_BUFFER
はいわゆる“可変長構造体”――最後のメンバが“実際の使用上は可変長”の配列である構造体――になっています。C言語で可変長構造体を扱うのは結構面倒ですが、FFIではこれを簡単に扱う仕組みがあります。上掲のプログラムのtypedefを見ると、可変長配列のメンバPathBuffer
のサイズ指定が1
ではなく?
となっています。
//可変長配列(VLA)の要素
WCHAR PathBuffer[?];
このように宣言しておくと、実際にREPARSE_DATA_BUFFER
型の値をffi.new()
で作成する際に、?
に入る値を引数で別途指定できます。
local uoplen = 32768
local rdata = ffi.new("REPARSE_DATA_BUFFER", uoplen)
つまり、上掲のコードはPathBuffer
の[?]
(あるいは元の[1]
)を[32768]
21に置き換えた固定長の構造体を実際には生成します。このrdata
のバイトサイズ(=65576)は通常通りffi.sizeof(rdata)
で取得できます。直観的ですね。
※ただし、本来のREPARSE_DATA_BUFFER
のような「“可変長共用体”を含む可変長構造体」はさすがに対応できないようです。“横着”をした一番の理由は、この辺りの困難を避けるためだったりします。
注意事項
- 例によって、Windows以外では、
lfs.readlink()
にフォールバックします。 -
bit32
はLua5.2標準のビット演算用モジュールです。(LuaTeXとLuaJITTeXの両方で使用できます。) -
LPSECURITY_ATTRIBUTES
とLPOVERLAPPED
は本当は特定の構造体へのポインタ型です。しかしこのプログラムではNULLポインタとしてしか使っていないので、横着をしてvoid*
にtypedefしています。 - Windowsの定数マクロ
INVALID_HANDLE_VALUE
の定義は(HANDLE)-1
で、HANDLE
はポインタ型です。このように“FFI特有の値”となる型にキャストする場合は、ffi.cast()
関数を使います。ffi.C.CreateFileW()
のHANDLE
型の戻り値とCcst.INVALID_HANDLE_VALUE
のポインタ値比較は普通に==
で行えます。 -
rdata.PathBuffer + sso
はポインタ演算です(C言語での同じコードと同じ意味)22。なので、オフセット値sso
はバイト単位でなくWCHAR
単位です。 -
ffi.sizeof()
は「C言語の値(FFIのユーザデータ)」についてバイトサイズを求めます。 - (細かい話ですが)構造体Sに対して、「S型のFFIのユーザデータ」は
S
型の引数にもS*
型の引数にも渡すことができて期待通り動作します23。 -
rdata.ReparseTag
がIO_REPARSE_TAG_SYMLINK
(=0xA000000C
)と等しいことを判定するところで難儀しました。実際に0xA000000C
である場合にrdata.ReparseTag
の値を読み出すと、ULONG
型(=uint32_t
)であるはずなのに何故か負の値(つまり(int32_t)0xA000000C
)が返ってきてしまいます(FFIのバグ?)24。仕方がないので、bit32.bor()
で無理やり正の値に変えることで回避しています。
事例⑦:帰ってきたshutwindown
本記事の前フリの中で、「WindowsをシャットダウンするLaTeX文書」(を作るためのtcshutwindownパッケージ)というネタを紹介しました。しかしこのパッケージはかなり昔に作ったものなので現在の版のLuaTeXでは動作しません。本記事の締めくくりのネタとして、FFIの機能を使って“shutwindown”パッケージを実装してみましょう!
shutwindownなLuaプログラム
まずはFFIでシャットダウンの処理を実装してみます。
local ffi = require("ffi")
require("ffi_typedef")
ffi.cdef[[
//// 型宣言
typedef void *HANDLE;
typedef HANDLE *PHANDLE;
typedef struct { //ローカル一意識別子
DWORD LowPart;
LONG HighPart;
} LUID, *PLUID;
#pragma pack(4)
typedef struct { //LUIDとその属性
LUID Luid;
DWORD Attributes;
} LUID_AND_ATTRIBUTES;
#pragma pack(8)
typedef struct { //トークンの特権セット
DWORD PrivilegeCount; //特権の要素数
LUID_AND_ATTRIBUTES Privileges[?]; //特権
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
//// プロトタイプ
//現在プロセスの擬似ハンドルを取得
HANDLE GetCurrentProcess( //戻り値:現在プロセスのハンドル
void);
//プロセスのアクセストークンを開く
BOOL OpenProcessToken( //戻り値:成功したか
HANDLE ProcessHandle, //入力:プロセスハンドル
DWORD DesiredAccess, //入力:アクセス権
PHANDLE TokenHandle); //出力:トークンハンドル
//特権名からローカル一意識別子を探す
BOOL LookupPrivilegeValueA( //戻り値:成功したか
LPCSTR lpSystemName, //入力:システムの名前
LPCSTR lpName, //入力:特権の名前
PLUID lpLuid); //出力:ローカル一意識別子
//アクセストークンを更新する [advapi32]
BOOL AdjustTokenPrivileges( //戻り値:成功したか
HANDLE TokenHandle, //入力:特権トークンハンドル
BOOL DisableAllPrivileges, //入力:全特権を無効化するか
PTOKEN_PRIVILEGES NewState, //出力:更新内容の特権情報
DWORD BufferLength, //入力:更新前のPreviousStateのサイズ
PTOKEN_PRIVILEGES PreviousState, //入出力:更新対象の特権情報
PDWORD ReturnLength); //出力:更新後のPreviousStateのサイズ
//直近のエラーコードを取得
DWORD GetLastError( //戻り値:直近のエラーコード
void);
//システムのシャットダウンを行う
BOOL ExitWindowsEx( //戻り値:成功したか
UINT uFlags, //入力:シャットダウン操作
DWORD dwReserved);
]]
Ccst = { -- 定数定義
TOKEN_ADJUST_PRIVILEGES = 0x0020;
TOKEN_QUERY = 0x0008;
SE_PRIVILEGE_ENABLED = 0x00000002;
SE_SHUTDOWN_NAME = "SeShutdownPrivilege";
ERROR_SUCCESS = 0;
EWX_SHUTDOWN = 0x00000001;
EWX_REBOOT = 0x00000002;
}
advapi32 = ffi.load("advapi32")
--- システムのシャットダウンを開始する
-- @return 成功したか
function shutdown_system()
-- プロセストークンの取得
local TokenHandle = ffi.new("HANDLE[1]")
local ok = ffi.C.OpenProcessToken(ffi.C.GetCurrentProcess(),
Ccst.TOKEN_ADJUST_PRIVILEGES + Ccst.TOKEN_QUERY, TokenHandle)
if ok == 0 then return false end -- 失敗
-- 必要な権限を取得する
local Privileges = ffi.new("TOKEN_PRIVILEGES", 1)
Privileges.PrivilegeCount = 1
Privileges.Privileges[0].Attributes = Ccst.SE_PRIVILEGE_ENABLED
ok = advapi32.LookupPrivilegeValueA(nil, Ccst.SE_SHUTDOWN_NAME,
Privileges.Privileges[0].Luid)
if ok == 0 then return false end -- 失敗
advapi32.AdjustTokenPrivileges(TokenHandle[0], 0, Privileges,
0, nil, nil)
--※AdjustTokenPrivilegesはGetLastErrorの確認が必要
if ffi.C.GetLastError() ~= Ccst.ERROR_SUCCESS then return false end -- 失敗
-- シャットダウンを開始する
ok = ffi.C.ExitWindowsEx(Ccst.EWX_SHUTDOWN, 0);
return (ok ~= 0) -- 成功したか
end
-------- 使用例
print(shutdown_system())
-- EOF
- ソース:shutwindown.lua
そして実行結果は……、アッ、実行すると本当にシャットダウンしてしまうのでチョット待ってください。
解説してみる
大部分は今までに見てきた事柄の復習なので説明は省略しますが、一点だけ説明すべきことがあります。
このプログラムではAdjustTokenPrivileges
というWindows APIを使っていますが、これは今までに出てきたAPIとは異なり、advapi32というライブラリに属します。advapi32はLuaTeXにリンクされたライブラリではないため、ffi.C
の下に配置されておらず既定では呼び出せません。
AdjustTokenPrivileges
を使うために、動的ライブラリ(DLL)の読込が必要です。これを行うのがffi.load()
関数です。
advapi32 = ffi.load("advapi32")
Windowsの場合25、これでadvapi32.dllが動的にリンクされ、そこに属するAPIが名前空間advapi32
の下に配置されます。つまり、Lua関数advapi32.AdjustTokenPrivileges()
を実行すればAdjustTokenPrivileges
が呼び出されるわけです。
shutwindownなパッケージ
Luaのコードが完成したので、早速「WindowsをシャットダウンするLaTeXパッケージ」を作りましょう。
この辺りは純粋にTeX言語(TeX on LaTeX)の話になるため、ここでは詳細は一切省略します。TeX言語中で一般のLuaのコードを実行することと比べて、FFIの使用について特に注意すべき事項はありません。
完成したshutwindownパッケージのソースは以下のようになります。
- ソース:shutwindown.sty
shutwindownパッケージの使用法は、かつてのtcshutwindownパッケージと全く同じで、猶予時間を指定してパッケージを読み込みます。
% <整数>は猶予時間(秒単位)
\usepackage[grace=<整数>]{shutwindown}
shutwindownな文書
Luaのコードが完成したので、早速「WindowsをシャットダウンするLaTeX文書」を作りましょう。
※FFIの機能を使うと本当に何でもできてしまうため(外部コマンド実行許可以上に)悪用の危険があります。このため、(TeXモードの)LuaTeXでFFIを利用するには無制限シェルエスケープ許可(-shell-escape
オプションを付ける)の状態でエンジンを起動する必要があります。
% LuaLaTeX文書; UTF-8
\documentclass[a4paper]{article}
\usepackage[grace=30]{shutwindown}% 30秒後にシャットダウン
\begin{document}
Hello Windows!
\end{document}
これで準備は整いました。素敵なシャットダウンで記事を締めくくりましょう!
>lualatex -shell-escape test-shutwindown.tex
This is LuaTeX, Version 1.0.4 (TeX Live 2017/W32TeX)
system commands enabled.
(./test-shutwindown.tex
LaTeX2e <2017-04-15>
……(中略)……
(./shutwindown.sty (c:/texlive/2017/texmf-dist/tex/latex/graphics/keyval.sty)
!!!!!!!!WARNING!!!!!!!!
System will be shutdown in 30 seconds...
System will be shutdown in 20 seconds...
System will be shutdown in 15 seconds...
System will be shutdown in 10 seconds...
System will be shutdown in 5 seconds...
System will be shutdown in 4 seconds...
System will be shutdown in 3 seconds...
System will be shutdown in 2 seconds...
System will be shutdown in 1 second...
System will be shutdown right now...
FAREWELL!
>
まとめ
LuaTeXはマジで万能です! LuaTeXでイロイロなものをつくって人生を楽しく過ごしましょう!!
-
残念ながら、このtcshutwindownパッケージは“Lua 5.1に基づく古いLuaTeX”を前提としたものなので、Lua 5.2に基づく現在のLuaTeXエンジンには対応していません。
[FFI]: http://luajit.org/ext_ffi.html
[LuaJIT]: http://luajit.org/ ↩ -
Lua処理系の別実装の一つで、JITコンパイラによる高速化を特徴としています。 ↩
-
FFIはLuaJIT専用のライブラリであり、本家のLuaエンジンでは使えません。LuaTeXのLuaエンジンは本家Lua(5.2版26)なのですが、なんと、LuaTeXチームが独自にFFIを本家Luaに移植して使えるようにしました。従って、LuaTeXでFFIライブラリを用いることができます。(なお、LuaTeXチームはLuaJITを組み込んだ“LuaJITTeX”エンジンも開発していて、これもTeX Liveで配布されています。当然、LuaJITTeXでもFFIを利用できます。) ↩
-
マサカリこわい……。
[GetCurrentProcessId]: https://msdn.microsoft.com/ja-jp/library/cc429112.aspx ↩ -
実際のAPIの解説では、
void
ではなくVOID
となっています。このVOID
はtypedefではなくvoid
に展開されるマクロです(void型に対するtypedefは不可)。ffi.cdef()
はプリプロセッサの機能は持たないので、同じようにVOID
マクロの定義することはできません。仕方ないので、ここでは直接void
を用いています。 ↩ -
実際にはLuaTeXでは
ffi
モジュールはffi
というグローバル変数に最初からロードされています(LuaJITTeXではそうではない)が、この挙動には依拠しない方がよいでしょう。 ↩ -
LuaTeXのLuaは5.2版(LuaJITは5.1版相当)なので、Luaの数値型に整数型と実数型の区別(5.3版で導入された)はないことに注意してください。 ↩
-
言語仕様上で文字列がバイト列であって“Unicode文字列”とは見なせない以上、“Unicode版のファイルAPIを使うLua”という概念は規定できないように思えます。 ↩
-
ちなみに、PerlではWin32モジュールの
GetShortPathName()
関数でショートネームを取得できます。 ↩ -
“ANSI版のAPI”とは「文字列とはANSIコードページ(CP932)のバイト列(
char *
型)だ」と見なすAPIのことで、対して“Unicode版のAPI”とは「文字列とは“Unicode文字”(WCHAR
型整数)の列(ワイド文字列、WCHAR *
型)だ」と見なすAPIのことです。WCHAR
型はJavaのchar
型みたいなものです。 ↩ -
恐らくバイト列は一旦コピーしてコピー先のアドレスを渡しているのだと思われます。constでない
char *
の引数として「Luaの文字列」を渡すこともできて、その場合にCの関数の処理で当該のバイト列が書き換えられたとしても、元の「Luaの文字列」には影響は及ばないようです。 ↩ -
ちなみに、FFIのユーザデータの値は、普通のLuaの値と同様にGCの対象となります。 ↩
-
Luaのユーザデータ(userdata)とは(Luaコードでなく)ホストプログラムによってのみ生成される特殊な値のことです。 ↩
-
“Cの配列”は、Luaのテーブルと同様に、添字(0起点)で値の読み書きができます。例えば、
spath[0] = spath[1] + 1
のような代入文が書けます。 ↩ -
a
が“Cの配列”(または他のchar *
相当のもの)、n
を整数として、ffi.string(a,n)
はa
の先頭n
バイトの列(n
を省略した場合はナル終端までのバイト列)からなるLua文字列を返します。 ↩ -
実は、LuaTeXに組み込まれたLuaFileSystem(
lfs
)モジュールは独自拡張がされていて、その中にはショートネームを取得するlfs.shortname()
という関数があります。しかし、その実装でもANSI版のGetShortPathName
を使っているため、今の例と全く同じ問題を抱えているようです。 ↩ -
ヘッダファイルではマクロで定義されていることが多いのですが、先述の通り、プリプロセッサはないので自前の“名前空間”を用意しました。宣言コードの中で
const
付きの変数(定数)にしたりenum
にしたりする方法もあるようですが、なぜか正常に動作しない場合があったので回避しています……。 ↩ -
実は、Unicode版APIにおける“Unicode文字”とは「Unicodeの符号位置の整数(高々21ビット)」のことではなく「UTF-16のコードユニット」のこと(つまり
WCHAR
は16ビット整数)だったりします。(これもJavaのchar
と同じ!) ↩ -
BOOL
は単にint
のtypedefであり、通常のC言語の慣習に従い、0が偽でそれ以外が真を表します。FALSE
は0
に展開されるマクロです。 ↩ -
ところで、これは疑問なんですが、
ffi.new("BOOL[1]")
とffi.new("BOOL[?]", 1)
は何が違うんでしょうかね。FFIのマニュアルを見てもよく解らない……。 ↩ -
なお、この32768という値は、Windowsの拡張パス名の最大長(32767文字)に対応するものです。 ↩
-
ただし、前述の通りFFIには「配列型のユーザデータ」という概念があり、
rdata.PathBuffer
は「WCHAR[32768]
型のユーザデータ」であるのに対し、ポインタ演算の結果のuopath
は「WCHAR *
型のユーザデータ」となるようです。 ↩ -
既に述べましたが、同様に、型Tに対して「“Tの配列型”のFFIのユーザデータ」は
T*
型と互換になります。 ↩ -
FFIのマニュアルの説明では「
uint32_t
の値をLuaで読む場合にはuint32_t
→double
のキャストが起こる」ことになり、この通りなら負の値にはならないはずなんですが……。比較対象の方をffi.cast("ULONG", 0xA000000C)
にすることを試しましたが、こちらは正の数になるので、結局ダメでした。 ↩ -
POSIXシステムの場合、例えば
ffi.load("z")
とするとlibz.so
が読み込まれます。 ↩ -
Lua5.3版を組み込んだLuaTeXの開発も進められているようです。
[いわゆる“texlua”]: http://d.hatena.ne.jp/zrbabbler/20151011/1444578663 ↩