LoginSignup
21
16

More than 5 years have passed since last update.

TeXで覗くシステムプログラミングの世界(えっ)

Last updated at Posted at 2017-12-24

これは「TeX & LaTeX Advent Caleandar 2017」の25日目の記事です。
(24日目は golden_lucky さん です。)

例によって前フリ

アドベントカレンダーもいよいよ今日が最終日ですが、何だか超絶アレなタイトルです。「システムプログラミング」って何なのでしょうか。チョットこちらの記事を見てみましょう。

本連載では、一番最後の「OSの提供する機能を使ったプログラミング」をシステムプログラミングの定義として話をすすめます。

なるほど。そういうわけで、本記事では、「TeXで、OSの機能を直接利用する」という超絶アレな世界をチョット覗いてみます。ただし、対象とするOSはWindowsに限定します。つまり、TeXでWindows APIを呼び出す、という話です。

※本記事で扱ったソースコードは以下のリポジトリに収録されています。

TeX言語でWindows APIの呼出ができる?

できません。TeX言語はTeXのプリミティブしか呼び出せません。

アレ、じゃあ記事が成立しない……、いえ、心配ありません。この記事は「TeX言語で覗く~」ではなくて「TeXで覗く~」です。なので、LuaTeXでLua言語を使うことにすればよいのです(強弁)。

LuaTeXでWindows APIの呼出ができる?

Luaでは、外部の(機械語の)実行ファイルを起動したり、機械語の拡張モジュールを読み込むことができます。過去(2012年)のアドベントカレンダーにこんなネタがありましたね1

ここで使われている拡張モジュール(shutwindown.dll)の実装では実際に「WindowsをシャットダウンするAPI」(ExitWindowsEx)を呼び出しています。

えっ、「そのshutwindown.dllはTeXで実装したのでもLuaで実装したのでもない(実際、C言語による実装です)ので、『LuaTeXでWindows APIを呼び出している』とは到底いえない」、ですか。確かにそうですね……。

LuaTeXでWindows APIの呼出ができる!

それはそうとして、このLuaLaTeX文書を見てください。

hello.tex
% 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}

これを、-shell-escape付きでlualatexでコンパイルします。

※プロンプトを>で表します。

>lualatex -shell-escape hello

sysprog-1.png

おおっ、なんと、TeXなのにシステムのダイアログが出現しました。スゴイ!

何が起こったのでしょう。-shell-escape付きなので、何か外部コマンドを実行した? でも\directluaの中のLuaコードをみてもコマンド行らしきものは見当たりません。代わりに、何やらC言語のようなコードがあって、そこにAPI名MessageBoxAが書いてあります。

FFIライブラリ、スゴイ

これは、FFIという仕組を利用しています。LuaFFIは元々はLuaJIT2で用意されているライブラリで、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でやってみましょう。

pid.lua
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

このLuaプログラムを“texlua”で実行すると、現在プロセスIDが出力されます。

実行結果(例)
>texlua pid.lua
Process Id = 9924

プロセスIDを得るAPI

ここではGetCurrentProcessIdというAPIを利用しています。このAPIの関数プロトタイプは次の通りです5

C言語
DWORD GetCurrentProcessId(void);

Windowsプログラミングでは基本的な型(数値型、ポインタ型)について、独特な別名(typedef)を使う習慣があり、DWORDもその一つです。DWORD型の定義は以下の通りです。

C言語
typedef unsigned long DWORD;

つまり、普通にC言語のプログラムを書く場合、GetCurrentProcessIdのAPIを使うためには上記の2行を前もって宣言する必要があるわけです。(もちろんこの記述はヘッダファイルとして提供されているのでそれを読み込むのが通例でしょう。)

FFIの利用の手順

LuaTeXではFFIライブラリはffiという名前のモジュールとしてプリロード6されているので、これをロードします。

Lua
local ffi = require("ffi")

そして、先述の「GetCurrentProcessIdを使うために必要なC言語の宣言のコード」の文字列を引数としてFFIの関数ffi.cdef()を実行します。これで実際にGetCurrentProcessIdを使う準備が行われます。(つまり、FFIはC言語の“宣言”だけを解釈するパーザを備えているわけです。)

Lua
ffi.cdef[[//この中にC言語(C99)の"宣言"を書く
typedef unsigned long DWORD;
DWORD GetCurrentProcessId(void);
]]

kernel32およびuser32に属するAPI(GetCurrentProcessIdも該当する)はffi.Cという名前空間(テーブル)の下に配置されます。従って、あとはffi.C.GetCurrentProcessId()を普通のLua関数のように呼び出せば、Windows APIのGetCurrentProcessIdの呼出が行われます。

Lua
local pid = ffi.C.GetCurrentProcessId()

ここで、GetCurrentProcessIdAPIの返した値が(DWORDの)9924だったとすると、Luaのffi.C.GetCurrentProcessId()の戻り値は「Luaの数値型7の9924」になります。このようにC言語のデータ型とLuaのデータ型の間は可能な限り“自然な形で”自動的に変換が行われます。従って、この後は変数pidが「C言語の関数から返った値」であることを意識する必要はありません。

事例②:五秒間待ってやる

Luaにはいわゆる“sleep”の機能がありません。これをWindows APIのSleepで実現してみましょう。

sleep.lua
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

実行すると、5から始まるカウントダウンが1秒ごとに表示されます。

実行結果
>texlua sleep.lua
5
4
3
2
1
Finish!

引数をもつAPIを扱う

Sleepの関数プロトタイプは次の通りです。

C言語
void Sleep(DWORD dwMilliseconds);

必要な宣言のコードを記した文字列を引数にしてffi.cdef()を呼ぶと、ffi.C.Sleep()が配置されます。ここまで先の例と同じです。ここで、「1秒待機する」には、SleepAPIのDWORD型の引数に1000を指定する必要がありますが、どうすればよいでしょうか。

先の例では関数の戻り値について「データ型は“自然に”自動変換される」ことを見ましたが、これは引数についても同様です。つまり、「Luaの数値1000」を与えれば、それが自動的にDWORD型に変換されます。簡単ですね。

Lua
ffi.C.Sleep(1000)

事例③:とあるOSの短形式名(ショートネーム)

Luaは現代のスクリプト言語の中では珍しく、言語自体ではUnicode文字を扱わず、文字列を常にバイト列として扱います。この性質のため、Windowsでの非ASCIIのファイル名の取扱は非常に厄介です。

ソレの名は。

※以下、ANSIコードページは932(日本語)であると仮定します。

WindowsのGUI操作ではファイル名に任意のUnicode文字を含めることができるため、コードページ932(CP932)にない文字のファイルは普通に存在しえます。例えば、ディレクトリC:\tmp\temp1に次の名前のファイルがあったとします。

sysprog-2.png

  • test file.txt(ASCII文字のみ)
  • test ☆彡.txt(非ASCIIだがCP932にある文字を含む)
  • test ☃♪.txt(非ASCIIでCP932にない文字を含む)

ファイル名を引数にとるLuaの関数を使った場合、「Luaの文字列はバイト列」という性質上8、ANSI版のAPIが使われることになります。従って、もしLuaスクリプトをCP932で書いているのであれば、非ASCIIのファイル名も期待通り取り扱えます。しかしこの場合、CP932にない文字のファイル名test ☃♪.txtはスクリプト中に書くことすらできません。

Luaスクリプト
--文字コードは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文字を含むファイルは(バイト列が食い違うため)全く見つけられなくなります。

Luaスクリプト
--文字コードは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.txtTESTFI~1.TXT
  • test ☆彡.txtTEST☆~1.TXT
  • test ☃♪.txtTEST♪~1.TXT(CP932にないは入らない)

従って、ショートネームのTEST♪~1.TXTであれば、Luaの関数(ANSI版のAPIに帰着する)で扱えないという問題は避けられそうです9

前置きが長くなりましたが、ここではWindows APIを使って、ファイルのショートネームを取得してみます。

実際にショートネームしてみた

ファイルのショートネームを取得するAPIは、それ自身、ANSI版GetShortPathNameAとUnicode版GetShortPathNameWがあります10。先述の通り、Luaの文字列はバイト列なので、GetShortPathNameAを使う方が理に適ってそうです。

shortname-1.lua
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

先ほどのC:\tmp\temp1ディレクトリで実行すると、正しいショートネームが表示されました。

実行結果
>texlua shortname-1.lua
TESTFI~1.TXT

バイト文字列の扱い

GetShortPathNameAの関数プロトタイプは次のようになっています。

C言語
DWORD GetShortPathNameA(LPCSTR lpszLongPath,
    LPSTR lpszShortPath, DWORD cchBuffer);

この中のLPCSTRLPSTRcharのポインタの型であり、バイト文字列(CP932の場合は“マルチバイト文字列”)を表しています。このAPIに対応するLua関数ffi.C.GetShortPathNameA()の引数はどうすればよいでしょうか?

入力用の引数lpszLongPath(対象のロングファイルネーム)の方は簡単で、「Luaの文字列lpath」を与えれば済みます。これで、Cの関数では当該のバイト列が置かれたメモリ領域の先頭のポインタが渡されます11

これに対して出力用の引数lpszShortPath(結果のショートファイルネーム)については、内容が可変となるバイトのバッファを与える必要があります。つまり「普通のLuaの値」ではなく「FFI専用の特殊な値」が要るわけです。そのような特殊な値を生成するためのFFIの機能がffi.new()関数です。

Lua
  local spath = ffi.new("char[?]", 512)

これで、512要素のcharの“Cの配列”(512バイト)が確保12され、その配列の先頭のポインタ(char *型)を表すユーザデータ13が変数spathに代入されます。このspathlpszShortPath引数(char *型)に渡せばよいわけです。

ffi.C.GetShortPathNameA()の実行が正常に完了すると、配列spathには結果のショートネームが格納された状態になっています。“Cの配列”はLuaで値が読み出せる14ので、ここから「Luaの文字列」を作り出すのは難しくありません。FFIではこの機能がライブラリ関数ffi.string()として用意されています。実際の使われ方を見ればその仕様は明らかでしょう15

Lua
  -- 配列spathの先頭のsplenバイトからなるLua文字列
  return ffi.string(spath, splen)

これで一件落着…しない

ところで、この節の最初で、ショートネームを取得する目的は「非ASCIIのファイル名を扱うため」としました。この目的は果たせているでしょうか? 実装したshort_path_name()を非ASCIIのファイル名に適用してみましょう。

Lua
--文字コードは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に決まってるでしょ」という前提です。

console-1.lua
-- 文字コードは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

実行結果は以下の通りです。スクリプト中で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文字が出力できます。参考としてこれの実装例を挙げておきます。

sysprog-3.png

事例⑤:メイク・ファイル名・ショート・アゲイン

文字コード変換とワイド文字列の扱いが判ったので、もう一度、「ファイルのショートネームを取得する」ことに挑戦してみます。

shortname-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

先と同じくC:\tmp\temp1で実行してみます。今度は非ASCIIのファイル名も正常に扱うことができました!

実行結果
>texlua shortname-2.lua
TESTFI~1.TXT
TEST☆~1.TXT
TEST♪~1.TXT

ショートネームが取得できたので、これを使って実際にLuaでファイルが扱えることを確かめてみます。shortname-2.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に変換できない文字があったかどうかを調べる(あった場合は結果の文字列はファイル名として使えないので失敗と見なす)ために、WideCharToMultiByteAPIの引数lpUsedDefaultCharを利用しています。これはBOOL19のポインタ渡しの引数であるため、通常のC言語のコードでは次のように書かれるはずです。

C言語
BOOL dflt = FALSE;
// 変換不能な文字があった場合、dfltが真に設定される
WideCharToMultiByte(acp, 0, uspath, usplen, spath, usplen * 3,
    NULL, &dflt);

しかし、FFIではC言語のアドレス演算子(&)に相当するものがありません。そこで、代わりに次のようなC言語のコードを想定して、これをFFIに翻訳します。

C言語
BOOL dflt[1] = { FALSE }; // dfltはBOOL*型
// 変換不能な文字があった場合、dflt[0]が真に設定される
WideCharToMultiByte(acp, 0, uspath, usplen, spath, usplen * 3,
    NULL, dflt);

つまり、BOOLの1要素の配列をffi.new()で用意すればよいわけです20

Lua
  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に次の名前のファイルがあったとします。

sysprog-4.png

  • original.txt(リンクでない普通のファイル)
  • symlink.txtoriginal.txtへのシンボリックリンク)

実際にreadlinkしてみた

readlink.lua
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

先ほどのC:\tmp\temp2ディレクトリで実行した結果です。リンク先のファイル名が正しく取得できています。

実行結果
>texlua readlink.lua
symlink.txt links to: original.txt

解説してみる

DecideIoControlで使用している構造体REPARSE_DATA_BUFFERは、実際には共用体のメンバを含む次のような複雑なものです。

C言語
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の部分を展開(残りのメンバは削除)することにします。

C言語
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ではなく?となっています。

Lua(cdef内)
    //可変長配列(VLA)の要素
    WCHAR  PathBuffer[?];

このように宣言しておくと、実際にREPARSE_DATA_BUFFER型の値をffi.new()で作成する際に、?に入る値を引数で別途指定できます。

Lua
    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_ATTRIBUTESLPOVERLAPPEDは本当は特定の構造体へのポインタ型です。しかしこのプログラムでは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.ReparseTagIO_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でシャットダウンの処理を実装してみます。

shutwindown.lua
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

そして実行結果は……、アッ、実行すると本当にシャットダウンしてしまうのでチョット待ってください。

解説してみる

大部分は今までに見てきた事柄の復習なので説明は省略しますが、一点だけ説明すべきことがあります。

このプログラムではAdjustTokenPrivilegesというWindows APIを使っていますが、これは今までに出てきたAPIとは異なり、advapi32というライブラリに属します。advapi32はLuaTeXにリンクされたライブラリではないため、ffi.Cの下に配置されておらず既定では呼び出せません。

AdjustTokenPrivilegesを使うために、動的ライブラリ(DLL)の読込が必要です。これを行うのがffi.load()関数です。

Lua
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パッケージの使用法は、かつてのtcshutwindownパッケージと全く同じで、猶予時間を指定してパッケージを読み込みます。

LaTeX
% <整数>は猶予時間(秒単位)
\usepackage[grace=<整数>]{shutwindown}

shutwindownな文書

Luaのコードが完成したので、早速「WindowsをシャットダウンするLaTeX文書」を作りましょう。

※FFIの機能を使うと本当に何でもできてしまうため(外部コマンド実行許可以上に)悪用の危険があります。このため、(TeXモードの)LuaTeXでFFIを利用するには無制限シェルエスケープ許可(-shell-escape オプションを付ける)の状態でエンジンを起動する必要があります。

test-shutwindown.tex
% 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!

>

sysprog-5.png

まとめ

LuaTeXはマジで万能です! LuaTeXでイロイロなものをつくって人生を楽しく過ごしましょう!!


  1. 残念ながら、このtcshutwindownパッケージは“Lua 5.1に基づく古いLuaTeX”を前提としたものなので、Lua 5.2に基づく現在のLuaTeXエンジンには対応していません。 

  2. Lua処理系の別実装の一つで、JITコンパイラによる高速化を特徴としています。 

  3. FFIはLuaJIT専用のライブラリであり、本家のLuaエンジンでは使えません。LuaTeXのLuaエンジンは本家Lua(5.2版26)なのですが、なんと、LuaTeXチームが独自にFFIを本家Luaに移植して使えるようにしました。従って、LuaTeXでFFIライブラリを用いることができます。(なお、LuaTeXチームはLuaJITを組み込んだ“LuaJITTeX”エンジンも開発していて、これもTeX Liveで配布されています。当然、LuaJITTeXでもFFIを利用できます。) 

  4. マサカリこわい……。 

  5. 実際のAPIの解説では、voidではなくVOIDとなっています。このVOIDはtypedefではなくvoidに展開されるマクロです(void型に対するtypedefは不可)。ffi.cdef()はプリプロセッサの機能は持たないので、同じようにVOIDマクロの定義することはできません。仕方ないので、ここでは直接voidを用いています。 

  6. 実際にはLuaTeXではffiモジュールはffiというグローバル変数に最初からロードされています(LuaJITTeXではそうではない)が、この挙動には依拠しない方がよいでしょう。 

  7. LuaTeXのLuaは5.2版(LuaJITは5.1版相当)なので、Luaの数値型に整数型と実数型の区別(5.3版で導入された)はないことに注意してください。 

  8. 言語仕様上で文字列がバイト列であって“Unicode文字列”とは見なせない以上、“Unicode版のファイルAPIを使うLua”という概念は規定できないように思えます。 

  9. ちなみに、PerlではWin32モジュールのGetShortPathName()関数でショートネームを取得できます。 

  10. “ANSI版のAPI”とは「文字列とはANSIコードページ(CP932)のバイト列(char *型)だ」と見なすAPIのことで、対して“Unicode版のAPI”とは「文字列とは“Unicode文字”(WCHAR型整数)の列(ワイド文字列、WCHAR *型)だ」と見なすAPIのことです。WCHAR型はJavaのchar型みたいなものです。 

  11. 恐らくバイト列は一旦コピーしてコピー先のアドレスを渡しているのだと思われます。constでないchar *の引数として「Luaの文字列」を渡すこともできて、その場合にCの関数の処理で当該のバイト列が書き換えられたとしても、元の「Luaの文字列」には影響は及ばないようです。 

  12. ちなみに、FFIのユーザデータの値は、普通のLuaの値と同様にGCの対象となります。 

  13. Luaのユーザデータ(userdata)とは(Luaコードでなく)ホストプログラムによってのみ生成される特殊な値のことです。 

  14. “Cの配列”は、Luaのテーブルと同様に、添字(0起点)で値の読み書きができます。例えば、spath[0] = spath[1] + 1のような代入文が書けます。 

  15. aが“Cの配列”(または他のchar *相当のもの)、nを整数として、ffi.string(a,n)aの先頭nバイトの列(nを省略した場合はナル終端までのバイト列)からなるLua文字列を返します。 

  16. 実は、LuaTeXに組み込まれたLuaFileSystem(lfs)モジュールは独自拡張がされていて、その中にはショートネームを取得するlfs.shortname()という関数があります。しかし、その実装でもANSI版のGetShortPathNameを使っているため、今の例と全く同じ問題を抱えているようです。 

  17. ヘッダファイルではマクロで定義されていることが多いのですが、先述の通り、プリプロセッサはないので自前の“名前空間”を用意しました。宣言コードの中でconst付きの変数(定数)にしたりenumにしたりする方法もあるようですが、なぜか正常に動作しない場合があったので回避しています……。 

  18. 実は、Unicode版APIにおける“Unicode文字”とは「Unicodeの符号位置の整数(高々21ビット)」のことではなく「UTF-16のコードユニット」のこと(つまりWCHARは16ビット整数)だったりします。(これもJavaのcharと同じ!) 

  19. BOOLは単にintのtypedefであり、通常のC言語の慣習に従い、0が偽でそれ以外が真を表します。FALSE0に展開されるマクロです。 

  20. ところで、これは疑問なんですが、ffi.new("BOOL[1]")ffi.new("BOOL[?]", 1)は何が違うんでしょうかね。FFIのマニュアルを見てもよく解らない……。 

  21. なお、この32768という値は、Windowsの拡張パス名の最大長(32767文字)に対応するものです。 

  22. ただし、前述の通りFFIには「配列型のユーザデータ」という概念があり、rdata.PathBufferは「WCHAR[32768]型のユーザデータ」であるのに対し、ポインタ演算の結果のuopathは「WCHAR *型のユーザデータ」となるようです。 

  23. 既に述べましたが、同様に、型Tに対して「“Tの配列型”のFFIのユーザデータ」はT*型と互換になります。 

  24. FFIのマニュアルの説明では「uint32_tの値をLuaで読む場合にはuint32_tdoubleのキャストが起こる」ことになり、この通りなら負の値にはならないはずなんですが……。比較対象の方をffi.cast("ULONG", 0xA000000C)にすることを試しましたが、こちらは正の数になるので、結局ダメでした。 

  25. POSIXシステムの場合、例えばffi.load("z")とするとlibz.soが読み込まれます。 

  26. Lua5.3版を組み込んだLuaTeXの開発も進められているようです。 

21
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
16