読者です 読者をやめる 読者になる 読者になる

yunomuのブログ

酒とゲームと上から目線

cabalでビルドをカスタマイズする

Haskell

 cabalファイルだけでは足りない事がある。どうしても単一のプログラムではできないこともあるし、TemplateHaskellでも生成できなくてやむなくソースコードを自動生成するスクリプトを書かざるを得ない事もあります。自動生成なんかやると何かに負けた気分になりますが、cabalでなければ普通にやっている事なので、何も問題は無い気がします。MakefileとかAntとか。

 具体的には、
(1) Haskell製コード生成プログラムをビルド
(2) シェルスクリプト経由でコード生成プログラムを実行
(3) 本体のプログラムをビルド
という事をやりたい。

 ところがcabalには直接的に外部のコマンドを実行してビルドする方法が無いみたいです。無いみたいなんですが、ビルドを細かく制御する方法はいくつか用意されています。
(A) Build-Type: Configureでautoconfで頑張る
(B) Build-Type: MakeでMakefileを書いて頑張る
(C) Build-Type: CustomでSetup.hsを書いて頑張る

 はっきり言って(A)で何でもできるんですが、それなりに面倒くさい。(B)は楽そうだけど、全部のターゲット(http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Make.html)を定義するのは結構面倒だし、そんなに色々出来る必要は無い。cabalの機能もそれなりに使いたいし。どうでもいいけど、Makeのときにcabal configureの時にconfigureファイルが無いとコケるのはバグかな。空のconfigureファイルがあればその後は問題なく通っちゃうし、configureスクリプト使ってないっぽいのでバグっぽい。誰も使ってないんだろうか。まあ(A)と(C)があれば使わないよな。

 そんなわけで、余程の事がなければCustomで足りると思うのでCustomを使って行こうと思います。

 ここで、こんな感じのプロジェクトを作ったと仮定します。この例はHaskellのCabalのマニュアルからのコピペ(http://www.haskell.org/ghc/docs/7.0.3/html/Cabal/authors.html)のBuild-TypeをCustomに変えただけのものです。

Name:           TestPackage
Version:        0.0
Cabal-Version:  >= 1.2
License:        BSD3
Author:         Angela Author
Synopsis:       Small package with two programs
Build-Type:     Custom

Executable program1
  Build-Depends:  HUnit
  Main-Is:        Main.hs
  Hs-Source-Dirs: prog1

Executable program2
  Main-Is:        Main.hs
  Build-Depends:  HUnit
  Hs-Source-Dirs: prog2
  Other-Modules:  Utils

 今回はこれを、program1をビルドするためにprogram2とシェルスクリプト(build.sh)が必要だということにしよう。

 これに加えて、cabal initした時に作られるSetup.hsがあります。
 こんなの。

import Distribution.Simple
main = defaultMain

 このままの場合は、Build-Type: Simpleの時と全く同じ動きをします。
 ちなみにimportの部分をDistribution.Makeに変えるとBuild-Type: Makeと同じ動きになります。

 つまり、cabalコマンドを実行した時にこのmainから始まるプログラムが実行されるわけなので、例えば

main = return ()

などとやると何をしても何も起こらなくなります。いや、helpは大丈夫ですけど。

 じゃあconfigureとかbuildとかtestの判別はどうやってるんだというと、単にdefaultMainの中でgetArgsしているだけなので、

import System.Environment
main = getArgs >>= print

なんてやってみると、このSetup.hsというものがどれだけ何でもアリなのかがわかるんじゃないかな。ただ残念ながらcabalに定義されてないサブコマンドを作る事まではできません。

 というわけで、getArgsの中身を見ながら処理を書いてもいいんですが、それだとMakefileと変わりません。ここではcabalが知り得る色々な情報を用意してくれるCabalパッケージのライブラリを使って処理を書くことができます。
Cabal: A framework for packaging Haskell software | Hackage

 具体的には、例えばbuildサブコマンドの前に処理を挟みたい場合はこう

import Distribution.PackageDescription (HookedBuildInfo)
import Distribution.Simple
import Distribution.Simple.Setup (BuildFlags)

main :: IO ()
main = defaultMainWithHooks simpleUserHooks{preBuildHook = hook}

hook :: Args -> BuildFlags -> IO HookedBuildInfo
hook args flags = ...

何ができるかは、おおむねUserHooks型のマニュアルを見ればわかるかと思います。
http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Simple.html#t:UserHooks

 これの中の、例えばbuildHookという、これはhookというよりはbuildコマンドを実行した時に行われる処理そのものなんですけど、そのbuildHook関数の定義がこうなってる。

buildHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()

この中で、

  • PackageDescriptionは*.cabalファイルの中の全体に関わるNameとかDescriptionの部分
  • LocalBuildInfoはExecutable節とかLibrary節みたいな部分
  • UserHooksは定義したUserHooksそれ自身
  • BuildFlagsはサブコマンドに渡したターゲットやオプション

になっています。
 まるっきり、なんの工夫もなく、cabalの実行時に得られる情報そのものです。
 そのものなので、例えばbuildコマンド実行時にinstallを実行したりもできます。

import Distribution.PackageDescription (HookedBuildInfo)
import Distribution.Simple
import Distribution.Simple.Setup (BuildFlags, defaultInstallFlags)

main :: IO ()
main = defaultMainWithHooks simpleUserHooks{buildHook = hook}

hook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
hook desc info hooks _flags = (instHook hooks) desc info hooks defaultInstallFlags

これでbuildだけを実行することはできなくなり、buildするとinstallを必ず実行するようになりますが、installコマンドを実行すると無限ループするので使い物にはなりません。

 ここで最初の例に戻ります。
 program1をビルドするためには(1)program2をビルドして(2)build.shを実行した後に(3)program1をビルドする必要があります。
 まあ、これだけの情報があればこれくらいの処理は簡単に作れそうに見えます。

 ところが、これが意外に面倒くさい。
 hooksに渡される情報はあくまでもコマンドに渡される引数とcabalファイルの情報だけなので、これからビルドするターゲットがどれかという情報は含まれていない。program1をビルドしようとしているのかprogram2をビルドしようとしているのかの区別は存在しないのです。
 なので、(3)program1をビルドする前に(1)program2をビルドして(2)build.shを実行する、みたいな順序付けは非常に面倒くさいのです。順序付けならcabalのBuild-Tools: program2でできますけど、プログラムの実行ができない。

 ただ、build処理内でprogram1とprogram2の区別はされていないものの、一応何を実行しようとしているかは知ることができます、BuildFlagsのbuildArgsで。
http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Simple-Setup.html#t:BuildFlags
 このフィールドには、buildサブコマンドに渡されたターゲットが格納されています。

% cabal build program1

を実行すれば["program1"]が、

% cabal build program1 program2

を実行すれば["program1", "program2"]が、

% cabal build

を実行すれば[]が入ってきます。
 お察しの通り最後のが厄介で、buildはターゲットを指定しないと全てのターゲットをビルドするんですが、その時は空リストが格納されています。でも落ち着いてPackageDescriptionのexecutablesから全ターゲットのリストが取れるので、それを使えばいいと思います。こんな感じで。

import Distribution.PackageDescription (HookedBuildInfo, executables, exeName)
import Distribution.Simple

main :: IO ()
main = defaultMainWithHooks simpleUserHooks{buildHook = \desc _ _ _ ->
    print (map exeName (executables desc))}

 あとは、buildArgsの中身を見ながら、program1がビルドされるかどうかを判断してprogram2をビルドしたり、System.Processモジュールを使って外部コマンドを実行したりすればいいんじゃないかと思います。最後は力技です。面倒くさいです。

 さらに面倒なことに、これら面倒な事をパッケージ化してライブラリとして公開したとしても、cabalにはSetup.hsをビルドするための依存ライブラリを記述する機能が無いので、そのライブラリが既にglobalかsandboxにインストールされていないと本体(program1とか)のビルドがコケる。
 これなんとかならないのかな。もはやCabalに機能追加するしか無い気がする。
 いやtargetのどれかのBuild-Dependsに書けば大丈夫なんですけど、targetに関係無い依存関係書きたくないよねぇ。私が使っているのは今のところprocessとdirectoryで、これらは本体でも使っているので問題は起きていませんけど。悩ましい。

Javaの例外のスタックトレースを操作する(しない)

Diary Java

 Javaで、例外を作成するメソッドを作りたくなることが数年に1回くらいある。で、大抵はそんなこと実はやらないほうがよくて、やらないんですけど、まあやることになったとします。やるなよ。

 そんでこんなメソッドを作る。

    private Exception makeException() {
        return new Exception();
    }

まあ本来はメソッドで切り分けようというくらいだから、例外のメッセージを作るのが面倒くさいとか、単純にnewで作って済まないとか、いろんな事情があるとは思うんですけど省略。

 このメソッドを、どこか別の機能から呼び出して使う。

    public void testFunction() {
        // 何か処理
        throw makeException();
    }

    private static Exception makeException() {
        return new Exception();
    }

こんな風にやった時、スタックトレースの一番上はtestFunctionになるのかmakeExceptionになるのか。
 つまりスタックトレースってThrowableオブジェクトがnewされた時に作られるのか、throwされた時に突っ込まれるのか。そういえば知らない。
 一応、Throwable#setStackTraceというメソッドがあるので、どちらも可能といえば可能。

 throwされた時に突っ込まれるなら例外の発生場所を正確に把握することができるんだけど、でもcatchした例外を再度throwしてもスタックトレースは変わったりしないので、実は答えは「newされた時」しかない。
 ただ、newされた時にスタックトレースが作られると、makeExceptionみたいなメソッドを作った時に、スタックトレースのトップがmakeExceptionになってしまう。
 いや別にmakeExceptionが一番上でもその次の要素を見ればいいじゃんという話もあるんだけど、ライブラリをいくつか経由してたりするとcauseのcauseのcauseとかになってたりして、下の方に行くほど重要な筈なのに何故か下に行くほど省略されていて、「え、スタックトレース4つしか表示されないの、これほとんどフレームワークの中じゃん」みたいになる。だからスタックトレースはできるだけ短くしておきたい。

 ということで、makeExceptionみたいなメソッドを作ろうとするとこういう感じになるわけです。

    private static Exception makeException() {
        Exception e = new Exception();
        StackTraceElement[] stackTrace = e.getStackTrace();
        e.setStackTrace(Arrays.copyOfRange(stackTrace, 1, stackTrace.length));
        return e;
    }

例外をnewした後にスタックトレースの一番上を取り除いて返す。これで例外の発生源はmakeExceptionの中ではなく、少なくともmakeExceptionを呼び出した場所になります。
 実際にはRuntimeExceptionを投げたくなったりとか色々あるのでGenericsを使って定義すると思うんですが、それはまあいい。
 途中でスタックトレースのサイズが1以上であることを決め打ちしたコードがあるけど、Throwableオブジェクトはnewした時点で絶対にスタックトレースは1以上になるので、まあいいじゃないですかこれくらい。length<=0でIllegalStateExceptionやErrorを投げるとかでもいいんですが。

 で、大抵はこれを使ってコードを書いている途中とかで、このメソッドが不要であることに気付いたり、余計に話をややこしくしている事に気付いたりして、もうちょっと上のレベルから設計を見直す事になります。
 気付かなかった事にして続けます。

 Exceptionの基底クラスのThrowableのコンストラクタの実装はこんな感じになっていて、

public class Throwable implements Serializable {
//(略)
    private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
//(略)
    public Throwable() {
        fillInStackTrace();
    }
//(略)
    public synchronized Throwable fillInStackTrace() {
        if (stackTrace != null ||
            backtrace != null /* Out of protocol state */ ) {
            fillInStackTrace(0);
            stackTrace = UNASSIGNED_STACK;
        }    
        return this;
    }    

    private native Throwable fillInStackTrace(int dummy);
//(略)
}

まあnativeの先までは見てないんですけど、dummyって何だよという。それはどうでもいいんですが、ここでおそらくスタックトレースを作っていて、いかにもスタックトレースが格納されていそうなstackTraceにはUNASSIGNED_STACKを入れている。このUNASSINED_STACKという定数は空配列なので、なんだこりゃということになる。

 getStackTraceの実装を見てみる。

    public StackTraceElement[] getStackTrace() {
        return getOurStackTrace().clone();
    }

    private synchronized StackTraceElement[] getOurStackTrace() {
        // Initialize stack trace field with information from
        // backtrace if this is the first call to this method
        if (stackTrace == UNASSIGNED_STACK ||
            (stackTrace == null && backtrace != null) /* Out of protocol state */) {
            int depth = getStackTraceDepth();
            stackTrace = new StackTraceElement[depth];
            for (int i=0; i < depth; i++)
                stackTrace[i] = getStackTraceElement(i);
        } else if (stackTrace == null) {
            return UNASSIGNED_STACK;
        }
        return stackTrace;
    }
//(略)
    native int getStackTraceDepth();
//(略)
    native StackTraceElement getStackTraceElement(int index);

getOurStackTraceを読んでいて、実際のスタックトレース取得処理はgetOurStackTraceがやっている。ちなみにprintStackTraceもgetOurStackTraceを呼んでいる。
 で、stackTraceが空じゃなければstackTraceを返して、空だった場合にはまたgetStackTraceDepthやgetStackTraceElementというnativeメソッドでどこからか取ってきている。
 なんでfillInStackTraceと同時にこの辺やらないんだろうと思ったけど、まあJNIの仕様的に配列をそのまま取得はできないし、コンストラクタで必ずfillInStackTraceが呼ばれるから内部状態エラーが起きる事もないし、ややこしいけどこれでいいんだろう。
 とにかく、これでめでたくスタックトレースJavaコードの中に出現することになりました。

 スタックトレースの構築方法がなんとなく想像ついたところで、よく見るとfillInStackはpublicメソッドなので、外から呼べる事に気づきます。というかjavadocに書いてあります。
 なので、なんか上の方で書いたmakeExceptionの中での怪しいスタックトレース操作なんてやらなくても、

    Exception e = makeException();
    e.fillInStackTrace();
    throw e;

とやれば、fillInStackTraceを呼んだ行が一番上に来るようなスタックトレースが構築されます。ただし、これだとfillInStackTraceを2回呼び出すことになるので、それが少々気になる。結構重いし。
 いや、そもそもこんなことやらなくていいんですけど。
 設計を見直そう。

 ところで、fillInStackTraceはfinalにはなっていないので、オーバーライドしてしまえばExceptionをnewしてもスタックトレースが作られなくなって、その分newがかなり軽くなります。
 スタックトレースが必要無い時に便利なテクニックです。

 Play FrameworkのHTTPレスポンスを返す時に使うRedirectやResultの基底クラスのFastRuntimeExceptionクラスは、こんな風にfillInStackTraceを潰してありますね。
play1/framework/src/play/utils/FastRuntimeException.java at master · playframework/play1 · GitHub
 かしこい。みんなもソース読もう。

 しかしこういう事をすると、前半でmakeExceptionの中でスタックのトップを消す処理をしていましたが、ああいうところでIndexOutOfBoundExceptionが出てしまうようになります。
 なのでスタックトレースが0やnullになることは無いなんて甘い予測を立てたりせずに、真面目にスタックトレースの深さをチェックしておいた方が良いでしょう。プログラミングって難しい。
 まあでもこの場合は、さすがにスタックトレースがnullになることはありませんよね。たぶん。いや、どうだろう。プログラミングって難しい。

ファイルとファイルディスクリプタとファイルのようなものたち

Diary

 プログラムからファイルを読もうとしたらどうするか。
 まずディスクを選んで、ディスクに書き込まれているファイルシステムを調べる。次にファイルシステムの管理情報に基づいて目的のファイルのデータブロックを探す。具体的にはファイルシステムの形式に従ってディレクトリを辿ったりテーブルを検索したりする。
 見つかったデータブロックからデータを読む。
 以上おわり。

 でも普通、というかLinuxMacでプログラムからファイルを読む時に、ディスクを指定したり、ファイルシステムの形式に従って管理情報やディレクトリの内容を読んだりはしません。
 大抵は、ファイルシステムext4(Linux)だろうがISO 9660(CD ROM)だろうがFAT(フロッピーディスクやメモリーカードなど)だろうが、だいたいパス名をopen()で指定して、read()で読むとかそういう感じだと思います。いわゆるopen/close/read/writeというやつ。

 これがどうなっているのかというと、単にファイルシステムのドライバがopen/close/read/writeという共通インタフェースを提供するようになっていて、ファイルシステム毎に適切なドライバを選んで呼び出しているというだけです。
 この共通インタフェースを仮想ファイルシステムといいます。

 ファイルシステムを仮想ファイルシステムから使えるようにする処理をマウントといいます。mountコマンドでやるアレです。
 マウントでは、ファイルシステムの種類やマウントポイントなどの情報をどこかのテーブルに登録しています。このテーブルはだいたいOSの内部にあって、あれ、なんでOS内にあるんだっけ? まあとにかくUnixLinux系OSでは大抵仮想ファイルシステムというのはOSカーネルに実装されています。FreeBSDのソースパス上でもsys/kern/vfs_xxx.cとかにあるので、結構中核にある感じですよね。まあ、権限とか排他制御の関係でOS内部にあるわけなんですが。
 そんなわけで、以降はUnix/Linux系OSでの実装を元に話します。

 マウントポイントというのは、マウントポイントです。
 Unix/Linux系OSの仮想ファイルシステムでは、ディスクやファイルシステムが違っていてもその全てのファイルシステムを"/"から始まるパス名で、あたかも一つのファイルシステムであるかのように扱っています。
 なので、mountコマンドを引数無しで実行すると出てくるように、"/"はext4でディスク1だけど"/var"はディスク2、"/mnt/cdrom"はISO9660でCD-ROMとか、そういうことになっています。open()でファイル名を指定した時に、このテーブルと照合されて、適切なデバイスとファイルシステムドライバが選択されるようになっています。

 つまり、open()というのは、「どのデバイスを」「どのファイルシステムドライバを使って」「どのファイルに対して」readやwriteなどの処理を実行するか、という情報を決定する処理なのです。
 そして、その対応表というか、決定した情報は、OS内部の管理表に登録されて、その表のエントリ番号が、いわゆるファイルディスクリプタになります。この管理表というやつがプロセスごとにあって、だから1プロセスが開くことができるファイル数の上限というものがあるわけですね。

 ところで、ファイルディスクリプタというものは、例えば標準入力は0、標準出力は1、標準エラー出力は2と決まっていたり、パイプやソケットのようなプロセス間通信やネットワーク通信なんかでも使いますし、その他にもデバイスを扱う時にも出てきて、こいつら全然ファイルじゃないじゃんみたいな事になります。
 ファイルディスクリプタというのは、要はデバイスやファイルシステムドライバやパス名を記録したエントリのキーでしかなくて、ここで登録されているパス名やデバイスというものは、別に具体的なディスクやファイルである必要は無くて、ファイルシステムドライバに、パス名でリソースを指定する機能(=open)と、readやwriteみたいな機能がありさえすれば良いわけです。
 あ、でもパイプやソケットはopen無いですね。まあこれらはパス名とは違う方法でリソースを指定してると思ってください。ソース見た方が早いしわかりやすいと思います。pipe()やsocket()の中で似たような事をやってます。

 要するにファイルシステムドライバにopenやreadやwriteという機能があれば、ファイルがあろうがなかろうが仮想ファイルシステムにマウントすることができます。
 そうやってマウントされたものの例としては、/devだったり/procだったり/sysだったりとか、ああいうOSにログインすると存在するんだけどディスク上には存在しないファイル群があります。

 /dev以下のファイルはハードディスクそのものとか、USBポートとか、コンソールとか、そういう入出力デバイスにつながっていて、これらに読み書きするとハードディスクに書き込んだり、USBポートに繋いだデバイスを制御できたりします。
 例えば以下のプログラムをLinux上で実行するとたぶんディスクの1セクタ目が読めます。Macだとパス名を"/dev/disk0"とかにするといいかも。あとたぶんroot権限が要ります。

#include <stdio.h>
int
main()
{ 
    int fd, i, size;
    unsigned char buf[512];
    fd = open("/dev/hda", 0);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    size = read(fd, buf, 512);
    for (i = 0; i < size; i++) {
        printf("%02x ", buf[i]);
    }
    printf("\n");
    close(fd);
    return 0;
}

最後の2バイトが"55 aa"になっていたら、「このディスクから起動できます」みたいな印です。余談ですけど。

 それと、/dev/ttyというデバイスは、読むとキーボード、書くとディスプレイという事になっているので、さっきのプログラムのパス名を"/dev/tty"に変えて実行すると、改行なりCtrl+Dなりを実行するまでのキー入力をそのままprintf()で書きます。
 ソケットやパイプはちょっと面倒なんだけど、同じくファイルディスクリプタを取得してread/writeすると、他のプロセスやネットワーク上のどこかのデータを読んだり書いたり、つまりデータをやりとりすることができます。
 /procなんかは、OS内部のプロセス情報をあたかもファイルのように見せている。読むとOS内部のメモリから情報をとってきたりしています。/sysも同じ。

 これらも単にハードディスクやキーボードのドライバ、もしくはプロセス間通信のモジュールに、read/writeなどの機能が実装されていて、そのモジュールがファイルディスクリプタのテーブルに登録されているだけです。データのやりとり、入力出力というのは、突き詰めればread/writeがあればなんとかなる事が多いので、まあファイルと同じように扱えれば気楽だよねという発想なんじゃないかと思います。

 標準入力、標準出力、標準エラー出力は、プロセス生成時にあらかじめ適当なデバイスを開いておいたというもので、シェルのパイプやリダイレクトなんかはこの辺のファイルディスクリプタを別なものに付け替える処理をしています。

 FUSEなんかも、仮想ファイルシステムでマウントして使えるようなプログラムを書くためのフレームワークみたいなものです。プロセスをファイルシステムとしてマウントしているみたいな感じ。

 なんか何の話をしてるのかわからなくなってきた。
 ソースかなんか見ながらやった方が面白かったかな。

ディスク拡張したのにディスク容量が増えないんだけど(ファイルシステムの話)

Diary

 例えばAmazon EC2のEBSのように、ディスクが仮想化されるようになり、そうするとディスクの容量を拡張するというよくわかんない事がお手軽にできるようになり、「ディスク容量増やしたんだけどOSから見ると増えてない。なんで」みたいな事が起こり、「ディスク容量を増やしてもファイルシステムの容量は増えないんだよ」というのを説明するのも面倒くさい。
 良い時代になりました。

 まだ二次記憶装置はハードディスクが多いだろうし、単に私が知ってるというのと、私の習慣でディスクって言葉を使います。が、たぶんCDもDVDもSSDもだいたい同じことです。

 ディスクにはメモリと同じようにデータ格納位置にアドレスが振ってあって、メモリは1バイトごとにアドレスが振られていますが、ディスクの場合は512バイトごとに振らてています。なぜ512バイトなのかは知りません。この512バイトをセクタとかディスクブロックと呼びます。

 ディスクは大抵アクセスは遅いけど大容量で不揮発なので、消えると困るデータはだいたいディスクに保存する事になるんですが、どうやって保存するのか。
 どうやってもなにも単にディスク上のアドレスを指定して書き込めばいいんです。

 例えばディスクに書き込むdk_write(addr, data)みたいな関数があったとしたら

    dk_write(0, "Yahho-");

みたいにやればディスクの0番地にデータを書き込んで、めでたくあなたのコンピュータは起動しなくなります。まあ起動しなくなるかどうかは構成しだいではありますし、ここでは全然関係ないんですけど、単に0番地にはだいたいOSの起動に必要な情報が書かれている事が多いので。ブートセクタとかMBRってやつ。
 dk_writeの中では、out命令とかでハードディスクに対してアドレスとデータが格納されたメモリ上の位置を送信しているだけです。暇だったらATAデバイスドライバのコードとか見てみるとわかるかもしれませんが、DMAとか書き込みスケジューリングとか色々あってわかりづらいかもしれません。私は面倒なのでATAの仕様を調べました。

 それで、この関数があれば一応データは保存できるようになりますが、0番地にはこのデータ、1番地にはこのデータ、みたいに覚えておくのは大変だし、512バイトを超えるデータはどうするんだという話になります。まあ、512バイトを超える場合は連続した領域に書き込めばいいんですが、それにしてもじゃあどこからどこまでが一つのデータなのかを覚えないといけなくなるし、外部断片化が起きるようになります。

 なんにしてもディスク上のデータを管理するなんらかの機構が必要です。
 そのなんらかの機構というやつがファイルシステムです。

 つまり、ファイルシステムというのは、ディスク上のデータの『どこからどこまでがどのデータか』という情報を管理しておく機構で、そのデータの管理単位のことをファイルといいます。
 そして、ここまででOSの話がほとんど出てきていないことからわかるように、ファイルシステムというものはディスク上のデータ管理の話であって、本来OSとはあまり関係ありません。厳密にはというか、狭義では。
 そんなわけで、LinuxでもNTFS用のドライバがあればWindowsのファイルを読むことはできますし、逆にWindowsでもext4とかのドライバがあればLinuxのファイルを読むことができます。

 ファイルシステムがやっている管理というのは、本でいうところの目次を作っているようなものです。本にはページ1つ1つに番号がついていて、目次には章節のタイトルとページ番号の対応が書いてある。基本的にはそれと同じです。この目次に書いてあるタイトルが、ファイルシステムではファイル名になります。

 具体的に、UFS/FFSやext2のようなやつの話をします。以降はFFSの事だと思ってください。FAT系やNTFSは全然違うみたいですし。
 で、FFSでは、ファイルシステムではディスクをデータ用の領域と管理用の領域とに分けています。

 データ用の領域には、データが格納されています。そのまんまですが、データ、つまりファイルの中身です。中身は中身だ。それ以上の情報は無い。
 あ、一応『ここでデータは終わりです』みたいな情報は入っています。
 ファイルシステム上のデータは、ディスク上の複数のセクタをまとめたブロックで管理されています。なぜまとめるかというと、ディスクは物理的な特性上、連続したセクタを読むのは速いけども飛び飛びの領域を読むのは非常に遅いので、できるだけデータはまとめておきたい。512バイトでは小さすぎる。それで4KBなり8KBなりのサイズでまとめて管理することで、読み書きを高速化しようとしています。
 ただあまり大きいと内部断片化か起きるので、適当に小さい必要もあって、その速さと空間効率のバランスを取った結果が8KBくらいだったらしいです。
 ちなみに2004年頃に発表された資料によると、初期のGoogle File Systemのブロックサイズは4MBだったらしいです。当時はデカいと思ったけど、今はそうでもないような気もするけども、アクセス速度を考えるとやっぱりデカい気もする。どうせストライピングしてるだろうから問題ないのかなぁ。おそらく今も変わらないか、小さくなるか、逆にめちゃくちゃデカくなってるか、いやよくわかんない。
 4KBやその倍数や4MBなのはメモリ管理の都合じゃないかな。メモリ管理的に、4KBより小さいのや4MBより大きいのは、現状ではあまり意味が無い気がします。

 管理用の領域には、ファイルシステムの大きさや種類などの基本的な情報や、inodeが格納されています。
 inodeというのは、目次の1行みたいなものです。ファイルのサイズ、中身のデータの格納場所(ブロック番号のリスト)、ファイルの種類(通常ファイル、ディレクトリ、シンボリックリンクなど)、パーミッションなどが記録されたテーブルです。基本的に1エントリが1ファイルを表しています。ただここにはファイル名は入っていません。
 inodeのエントリは固定長なので、番号でアクセスすることができます。この時の番号をinode番号と呼びます。inode番号は"ls -i"などのコマンドで見ることができます。
 ちなみにinodeはIndex-Nodeの略だという人もいますが、実のところは何の略だかよくわかってないらしいです。どうでもいいですけども。

 で、ディレクトリというのがあります。
 ディレクトリというのは、ファイルの一種です。
 ディレクトリ・ファイルは、簡単に言うとファイル名とinode番号が格納されたテーブルです。ファイル名はディレクトリに保存してあります。
 つまり、"/bin/ls"というファイルを読もうとした時には、以下の様な手順を踏みます。
(1) "/"のディレクトリを指すinodeを読む("/"のinode番号だけはファイルシステムの管理領域に書いてある)
(2) (1)で読んだinodeから"/"ディレクトリの内容が格納されたブロックを特定し、読む
(3) (2)の内容から、"bin"のエントリを見て、"bin"のinode番号を特定する
(4) "bin"のinodeを読む
(5) "bin"の内容が格納されたブロックを読む
(6) (5)から"ls"のエントリを見て、"ls"のinode番号を特定する
(7) "ls"のinodeを読む
(8) "ls"の内容が格納されたブロックを読む(終わりだよ〜)

 まあ結構面倒くさいわけです。DNSとだいたい同じです。
 ただ、これでファイル名があればファイルの中身にたどり着くことができるようになりました。
 おおむね、ファイルシステムはこんな感じの仕組みになっています。

 このような仕組みになっているので、以下の様な現象が起きます。

 lnコマンドで作れるハードリンクというものがあります。これはファイルの実体は同じものなんだけど、パスや名前が違うファイルを作ることができる機能です。これは違うファイル名に対応するinode番号を同じものにすることで実現することができます。
 ハードリンクはどっちがリンク先リンク元という区別はなくなり、どちらもまったく同じ立場のファイルになります。
 ただし、inode番号というのは定義上ファイルシステムに固有のものなので、違うファイルシステム上のファイルにハードリンクを張ることはできません。

 mvコマンドによるファイルの移動は、同じファイルシステム内であればinode番号は変わらずに、移動元と移動先のディレクトリの情報を書き換えるだけです。ディスク上のデータの格納位置も変わりません。

 ファイルの削除というのは、ディレクトリ内のファイル名とinode番号のエントリを削除したり、inodeからブロック群への参照を削除したりする処理です。『ファイルを削除してもデータは残る』と言われる所以はこれです。それと、ファイル削除のことをunlinkと言ったりするのもこれが理由です。というか、ファイルシステムのインタフェース的にも削除じゃなくてunlinkです。複数のハードリンクがあると消えませんし。

 ところでパーティション(スライス)というのはディスクを複数に分割する機能で、それぞれに異なるファイルシステムを構築することができます。
 なのでパーティションが違うと違うファイルシステムとみなされて、ハードリンクを張ることはできません。
 また、mvコマンドでディスクやパーティションをまたいでファイルを移動させようとすると、移動ではなくてコピーになります。いや最近はコピーした後に移動元を削除してくれますけども。

 ちなみにシンボリックリンクというのはリンク先のファイル名が書かれたファイルなので、リンク先が他のパーティションだろうが他のホストだろうが一時的にしか存在してなかろうがファイル名でアクセスできる限りなんでもアクセスできますが、ファイル名が変わったりするとアクセスできなくなります。

 で、えー、なんだ。話を戻すと、
 ファイルシステムは管理領域とデータ領域に分かれていて、管理領域の中のinodeは固定長で、番号でアクセスできるようになっている。データ領域も8KBだかのブロックに分かれていて、これも番号が振られている。
 この構造自体がディスクに書き込まれている情報なので、一度作ってしまうと、例えばデータ領域に一切変更を加えずに管理領域の容量を増やしたり減らしたりするのは難しい。管理領域のサイズやデータ領域の構造に少しでも変更を加えると全データが使えなくなる可能性がありますし、普通は管理に必要な領域以外は全部データ領域にするので、変更の余地が無いわけです。
 この、ファイルシステムの構造をディスクに書き込む処理の事を『フォーマット』といいます。ファイルシステムの構造を作るのでフォーマットといいます。フォーマットでファイルシステムの内容が全部消えるのはこのせいです。

 そして、inodeは中盤でちょっと触れたように、1エントリが1ファイルに対応しています。そして、ファイルシステム作成時に管理領域の大きさも決めてしまわないといけないので、inodeのエントリ数もファイルシステム作成時に決めてしまわないといけなくなります。
 つまり、ファイルシステム作成時に、そのファイルシステムで管理できるファイル(ディレクトリ等含む)の最大数を決める必要があります。inode数を多くするとファイル数を増やせますが、管理領域が増えるのでデータ領域が減ります。小さいファイルを多数作るならそれもアリでしょう。

 また、データ領域のブロックサイズも作成時に決める必要があります。
 これもまた、大きいと読み書きは速くなりやすくなりますが、小さいファイルが多いと内部断片化しやすくなります。小さいと効率は良くなりますが、大きいファイルを扱う時に外部断片化が起きやすくなって、アクセスは遅くなりやすくなります。デフラグすりゃいいんですが、大変ですし。
 デフラグというのは、この場合は飛び飛びになったデータを並び替えてできるだけ一箇所にまとめることです。一箇所にまとめた方がディスク上の効率が良い。SSDだと関係ないんですけど。

 そんなわけで、実はファイルシステムを作る時というのは結構色々な事を考えないといけないのです。どれくらいの大きさのファイルをどれくらいの数作るか。
 最近はディスクが安いからかなんなのか、そこら辺はあまり神経質に考える必要がなくなっています。が、mkfsコマンドのオプションを見るとまあ一応色々細かく指定することができるようにはなっています。
 昔はOSをインストールするといったらこんな風にまず用途を考えてファイルシステムのパラメータを色々と調整するところから始めないといけなかったそうです。時代は変わって、特に何も考えなくてもOSをインストールできるようになりました。素敵ですね。

 そう、で、だから、ディスク容量を増やしてもinodeの数も増えないしデータ領域のブロック数も増えないからファイルシステム上の容量は増えないわけです。
 ただ、これらの管理情報を後から拡張する方法がファイルシステムに用意されていれば容量を増やすことはできますし、単に新しく作ったファイルシステムに情報をコピーしても増やすことはできます。どちらが簡単かはそれこそファイルシステムによりますね。
 減らすのが難しいのもこういう構造だからです。

 それと途中で「狭義のファイルシステム」みたいな事を書いたと思いますが、じゃあ広義ってなんだというと、ディスク上の構造ではなく、インタフェースとしてのファイルシステムという概念があります。
 この辺から、OSとかユーザ寄りの話になってきて、これはこれでかなり面白いんですが、別の話なので別な時に。
 キーワードは仮想ファイルシステムとか、まあ単にファイルシステムでもいいです。あとはreadカーネルコールの処理を追いかけてみるとか。

パッケージ管理システムのこと

Diary

 自分で作ったプログラムを実行することは、ほとんどの場合で簡単にできます。だいたい動作確認しながら作るので当たり前です。
 でも自分で作ったプログラムを人に渡して使ってもらおうとすると、これが結構面倒くさい。
 ここでプログラムというのは、実行ファイルというかOSのロードモジュールの事を指すとします。

 まず、渡した相手のコンピュータのCPUが違うと動かない。PowerPCやARM用のプログラムはIntelアーキテクチャのプロセッサでは動かない。
 動的リンクライブラリを使っている場合は、動的リンクライブラリのパスが違うと動かない。
 そもそも依存しているライブラリがインストールされていないと動かない。
 設定ファイルを使う場合はパスやファイル名が違うと動かない。
 プログラムファイルの形式が、OSのローダが対応しているものでないと動かない。ELFなのかMach-OなのかPortable Executableなのか。
 OSの機能を呼び出すなら、呼び出し方が合っていないといけないし、呼び出す機能が存在しないといけない。
 デバイスを使っている場合は、デバイスの指定方法やアクセス方法が違うと動かない。まあこの辺はだいたいOSに任せるけども。
 などなど、結構面倒くさい。

 実のところこれらの面倒くさいあれこれは、プログラムをソースコードのまま渡すことである程度解決します。
 通常、コンパイラは自分の環境で動くプログラムを出力するので、CPUが違って動かないとか、プログラムの形式が違って動かないとかいう事は起きません。
 動的リンクライブラリにしても、パスはコンパイル時(正確にはリンク時)にプログラムに埋め込んでくれるので、インストールしてさえあればおおむね大丈夫です。
 自分の環境で動かないという事があれば、なんだったらソースコードを直してもいい。ヘッダファイルに環境依存情報(ファイルパスとかメモリ量とか)が書いてあって、そこをちょっと直すと動くという事は、よくあるというかあったというかあったらしいというか想像に難くない。

 そういう環境依存でのコンパイル段階でのトラブルを避けるために、ヘッダファイルやマクロを使って色んな環境でコンパイルできるように工夫します。例えば、

#if VERSION <= 5
#include "env5.h"
#endif

などとやると、VERSIONマクロが5以下の時だけenv5.hというヘッダファイルが読み込まれます。
 こういう風に、OSが違ったら、バージョンが違ったら、ライブラリがインストールされてなかったら、int型のバイト数が違ったら……と、様々な状況を想定してコードを書きます。
 場合によっては、環境に依存したヘッダファイルを生成するためのプログラムを作って、コンパイル前に実行したりします。OSやコンパイラコンパイルではたぶん必ずやっています。

 そうなってくると、コンパイル時に色んな事を考えないといけなくなります。どういう手順でコンパイルすればいいのか、マクロ変数の値をなんにしたらいいか、ヘッダファイルはどれを使えばいいのか、そもそもどのファイルをコンパイルしてリンクすればいいのか。
 いろいろあって面倒なので、たいていそういう情報はMakefileに書いてあります。というかこれくらい色々あると開発中でも面倒なので、そういうコンパイル手順のアレコレはだいたいMakefileに書いてあります。なので、

% gcc -o program src1.c src2.c src3.c -Iinclude -L/usr/local/lib -DARCH=I386

みたいにやっていたのが、

% make

とかで一発で実行ファイルができあがる。便利!

 ところが、まだまだそううまくいかない。
 Makefileには、プログラムのコンパイルの手順が書いてあるんですが、中身は単にコマンドの羅列なので、コマンドが無かったり名前が違ったりすると動きません。ファイルを移動するmvコマンドがmoveだったり、whichが無かったり、ライブラリが変な場所にインストールされていたりするとコンパイルできなかったりします。
 さらに、仮にプログラムがコンパイルできたとしても、動的リンクライブラリが無かったりパスを間違えていたりすると実行できません。

 これを解決するのがautoconfだったりconfigureスクリプトです。
 よくプログラムをソースからインストールする時に

% ./configure
% make
% make install

みたいな事をすると思いますが、この"./configure"というやつ。これを実行すると、コンパイルに必要なライブラリやコマンドやヘッダファイルがインストールされているかをチェックして、環境に適したMakefileを生成してくれます。
 configureが無い場合はまたそれを生成するツールなんかもあったりします。ここらへんは、だいたいREADMEとかMakefileの頭のコメントとか適当に実行した時のエラーメッセージに書いてあるし、慣れればファイル構成を見ただけでどうすればいいかわかるようになります。

 これでもうソースコードで公開されているソフトウェアの大半はコンパイルして使う事ができるようになる気がします。が、そもそもそのソースコードはどこで配布されてるのか。まあ今だったらググれば大抵公式サイトが出てくるのでそこから取ってくればよろしい。でも公式サイトがわからなかったり、検索エンジンが無い時代だったりすると、探すのが大変だったり、人に教えるのも大変だったり。
 バージョンアップされた時も、更新した情報をどこからか入手して、ソースコードを入手して、コンパイルする必要があって、結構面倒です。

 そこで、公開されている便利なプログラムの一覧を管理するシステムができます。とりあえず私が知ってるのはFreeBSDportsなのでportsの話をします。
 portsは、プログラムのリストを持っていて、例えばemacsなら/usr/ports/editor/emacs/とか、curlなら/usr/ports/net/curl/という風に、カテゴリごとに分けたディレクトリに情報が保存されています。いや正確なパスなどは忘れましたが。これで、例えば

% cd /usr/ports/editor/emacs
% make && make install

みたいにやるとめでたくemacsがインストールされます。

 例えばこの場合の/usr/ports/editor/emacsにあるMakefileが何をやっているかというと、emacsソースコードを配布しているサイトからソースコードと、場合によっては修正パッチが入ったファイルをダウンロードしてきて、解凍してパッチを当ててコンパイルしている。ソースコードがどこで配布されているかとか、パッチはどれかとか、そういうのは全部このMakefileやそれが呼び出すシェルスクリプトなんかに書いてあって、自動的に処理してくれる。といってもまあそういうのもパッケージの管理者が書いていてくれているんですが。

 で、バージョンアップの時はどうやっているのかというと、まあバージョンアップがあった事を知るのはやっぱりこまめな情報収集が必要だったりするんですけども、このportsのディレクトリ構成自体がCVSとかを使ってどこかのマスターデータと同期するようになっていて、誰かがソフトウェアを更新するとパッケージ管理者がportsのマスターデータを修正して、利用者はデータ同期してその情報を持ってきて、あとは最初と同じように"make && make install"で更新するというか上書きする。
 非常にシンプルで想像しやすい。

 ただ、portsは基本的にはソースコードからインストールする構造になっているので、インストールに結構時間がかかる。
 もちろん最初の方で書いたように、ソースコードからコンパイルした方が安全ではあるものの、例えば同じOSを使っていてハードウェアもOSで吸収できる程度の違いしか無いような環境だとか、実行ファイルやOSに互換性がある場合なんかは、あるコンピュータ上でコンパイルした実行ファイルがそのまま他のコンピュータにコピーしても動いたりします。
 というかWindowsなんかはそうだし、最近のOSはだいたいそうですね。
 そうなると、わざわざソースコードをダウンロードしてコンパイルするよりも実行ファイルをダウンロードした方が圧倒的に速いのでそうしようとなる。コンパイルは、どこかの似たような環境の誰かがやってくれていればいい。
 ただし、依存する他のプログラムやライブラリなんかが存在しないとやっぱり動かないので、その辺はきちんと把握しておく必要がある。
 ということで、バイナリパッケージというのが生まれた、のだと思います。

 例えばRed Hat LinuxRPMなんかは、パッケージをrpm形式のファイルで管理していて、rpmファイルの中には実行ファイルやライブラリや設定ファイルが入っている。それと、それらのファイルをどこに置けばいいのかとか、あとは依存する外部ライブラリの名前なんかの情報も書かれています。
 rpmファイルをインストールすると、中の情報にしたがって適切にプログラムやファイルを配置してくれて、RPMが持っているデータベースに情報が登録されます。これで現在どんなパッケージがインストールされているかを管理することができるようになっていて、例えば依存するパッケージがインストールされていなかった場合にはエラーを出してインストールが失敗します。
 RPMの場合はこんな感じで、自分でRPMファイルを探してきて、依存するパッケージも自分で探してインストールする必要がありますが、パッケージ管理の基本としてはそんな感じです。

 もちろんそれだと面倒くさいので、Yumみたいなのが登場します。
 Yumは、パッケージ名を指定するとRPMファイルをダウンロードするだけでなく、依存するパッケージがある場合はそれらもインストールしてくれるとても便利なものです。もちろんyumも中央で管理されているリポジトリがあって、それが更新されることでパッケージのバージョンアップにも追随できます。これまでの問題を概ね全部解決できるような素敵なシステムです。
 Debian使ってたからdpkg/APTで説明すればよかった……。まあAPTも同じような感じです。
 YumやAPTのみならず、RubyのgemとかJavaMavenとかHaskellのcabalとかもおおむね同じ仕組になっています。いやcabalはソースからビルドだからportsに近いし、gemもコンパイル無いし、MavenはバイナリだけどJavaだし、仕組みは似てるけど違うか。

 そしてこれより先の事は知らん。

 というのが、私が認識しているパッケージ管理システムの話です。実際のところは、APTあたりからこの世界に入ってきたのでよくわかりません。
 いや、そういえば数ヶ月前にこの辺の事を教えてと言われていたので。
 だからどうってことは無いんですけども、歴史というか、流れをある程度知っていると次の新しいものを理解しやすかったりするし、なんだったら作れる可能性もできるし、面白いんじゃないですかね。

 で、そもそも質問ってなんでしたっけ。

死んでもTwitterで遊ぼう

Diary

 先生が「死ぬまでTwitterで遊ぼう」みたいな事を言ったら、学生に「いや別に死んでもTwitterできますよプログラマなんだから」みたいな事を言われた、という話を去年京都で飲んだ時に聞いた。
 そういえば私は昔、人間を作りたくて色々勉強してた事を思い出した。いや今も作りたいんですが。

 で、とりあえずそれっぽく見えるものだけでも作ろうと思って、作ったのがこれ。botです。

 仕組みは単に、元ネタの文章を形態素解析して、前の語に続く語を出現頻度に応じてランダムに選んでいるだけ。文はmecabで解析して、作った辞書はredisに突っ込んでる。

 ついでにサーバ引っ越しとかなんとかもいっしょにやってたので20時間くらいかかったというか、久しぶりに気がついたら朝になっているというやつをやりました。
 なぜかテンションが上がりすぎて、1週間くらいかけて遊ぼうと思ってたのに1日で終わってしまった。まあまだ改良はいくらでもできるんですけど。
 なんかこういう、気がついたら朝になってたみたいなのは5〜6年ぶりな気がしますけども、楽しいは楽しいんですが食事も睡眠も忘れるというのはあまり健康には良くないんじゃないかという気もしますね。楽しかったけど。

 このbotの元データとしては私のTwitterのpostを使っていて、1万ツイート1MBくらいで、辞書の構築には1分半くらいかかる。
 さすがに元データが少ないので、元の発言とほとんど変わらないものが出てくる事がそれなりにあって微妙なんですが、そこら辺はなんか突然変異みたいな仕組みでも入れてやってみようかなぁと思っているところです。
 でも試しに私が書いた10MBくらいのテキストを食わせてみたところ、かなり意味わかんない言葉を生成できるようになったので、辞書のデータ量を増やした上で言葉を覚えさせる方向に改良していった方がいいような気がします。
 ただ10MBのテキストからの辞書構築には3時間かかったのでちょっと面倒ですね。いや構築途中でもしゃべれるのでいいんですが。アルゴリズム見直すのも面倒くさいし。

 そのうち記憶とか知覚とかも実装したいというか、2歳児くらいにはならないもんかなと思っているんですが、どうしたものだろうか。外界からの刺激を受け取るなり与えるなりさせないといけないんだけど、どう処理しようかなぁ。
 などと考えているうちにGWの残り2日が終わりそうです。

 @yunomu11がbotに延々話しかけるだけのアカウントになっていてもそっとしておいてください。

Android SDKのSQLiteDatabase#execSQLのbindArgsの型

Java Android

AndroidSDKのSQLiteDatabase#execSQLのマニュアル
[http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#execSQL(java.lang.String, java.lang.Object[])]
よくあるsqlとbindArgs(Object配列)を渡す構造なんですが、このbindArgsのマニュアルを見ると

Parameters

sql the SQL statement to be executed. Multiple statements separated by semicolons are not supported.
bindArgs only byte[], String, Long and Double are supported in bindArgs.

byte[], String, Long and Doubleのどれかしかサポートしてないよと書いてあります。

すると「え、でもInteger入れたいんだけどどうなのよ」ってなるわけじゃないですか。
で、中身を見てみると

SQLiteDatabase.java

    public void execSQL(String sql, Object[] bindArgs) throws SQLException {
        if (bindArgs == null) {
            throw new IllegalArgumentException("Empty bindArgs");
        }
        executeSql(sql, bindArgs);
    }

ここはnullを弾いてるだけ。

SQLiteDatabase.java

    private int executeSql(String sql, Object[] bindArgs) throws SQLException {
        acquireReference();
        try {
            if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) {
                boolean disableWal = false;
                synchronized (mLock) {
                    if (!mHasAttachedDbsLocked) {
                        mHasAttachedDbsLocked = true;
                        disableWal = true;
                    }
                }
                if (disableWal) {
                    disableWriteAheadLogging();
                }
            }

            SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs);
            try {
                return statement.executeUpdateDelete();
            } finally {
                statement.close();
            }
        } finally {
            releaseReference();
        }
    }

ここはSQLiteStatementのコンストラクタに渡して、その後にSQLiteStatement#executeUpdateDeleteを呼んでる。

SQLiteStatement.java

    SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
        super(db, sql, bindArgs, null);
    }

コンストラクタでは親クラスのコンストラクタに渡しているだけ。おそらく単に親のフィールドに格納していて、getBindArgs()で取ってきたりしているんだろう。というかしてた。ちょっと気を使った書き方になってたのでライブラリ作る人は見てみてもいいかも。

SQLiteStatement.java

    public int executeUpdateDelete() {
        acquireReference();
        try {
            return getSession().executeForChangedRowCount(
                    getSql(), getBindArgs(), getConnectionFlags(), null);
        } catch (SQLiteDatabaseCorruptException ex) {
            onCorruption();
            throw ex;
        } finally {
            releaseReference();
        }
    }

executeUpdateDeleteメソッドでは、getBindArgsで親クラスから取ってきた値をSQLiteSession#executeForChangedRowCountに渡している。

SQLiteSession.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
            return 0;
        }

        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
        try {
            return mConnection.executeForChangedRowCount(sql, bindArgs,
                    cancellationSignal); // might throw
        } finally {
            releaseConnection(); // might throw
        }
    }

ここでは、2箇所でbindArgsが使われているけれど、1つ目のexecuteSpecialの中では使われていなかったので、2つ目のSQLiteConnection#executeForChangedRowCountに渡されている方が本命。

SQLiteConnection.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        int changedRows = 0;
        final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
                sql, bindArgs);
        try {
            final PreparedStatement statement = acquirePreparedStatement(sql);
            try {
                throwIfStatementForbidden(statement);
                bindArguments(statement, bindArgs);
                applyBlockGuardPolicy(statement);
                attachCancellationSignal(cancellationSignal);
                try {
                    changedRows = nativeExecuteForChangedRowCount(
                            mConnectionPtr, statement.mStatementPtr);
                    return changedRows;
                } finally {
                    detachCancellationSignal(cancellationSignal);
                }
            } finally {
                releasePreparedStatement(statement);
            }
        } catch (RuntimeException ex) {
            mRecentOperations.failOperation(cookie, ex);
            throw ex;
        } finally {
            if (mRecentOperations.endOperationDeferLog(cookie)) {
                mRecentOperations.logOperation(cookie, "changedRows=" + changedRows);
            }
        }
    }

真ん中あたりのbindArgumentsで使われている。これ以降には出てこないので、たぶんここでstatementの中に入っちゃってるんだろう。nativeExecuteForChangedRowCountまで見れば処理の本体が見られるだろうけど、今回の興味はここではない。
ところでこのtry節のネストっぷりは、なるほどなぁと思った。次からこうしよう。

SQLiteConnection.java

    private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
        final int count = bindArgs != null ? bindArgs.length : 0;
        if (count != statement.mNumParameters) {
            throw new SQLiteBindOrColumnIndexOutOfRangeException(
                    "Expected " + statement.mNumParameters + " bind arguments but "
                    + bindArgs.length + " were provided.");
        }
        if (count == 0) {
            return;
        }

        final int statementPtr = statement.mStatementPtr;
        for (int i = 0; i < count; i++) {
            final Object arg = bindArgs[i];
            switch (DatabaseUtils.getTypeOfObject(arg)) {
                case Cursor.FIELD_TYPE_NULL:
                    nativeBindNull(mConnectionPtr, statementPtr, i + 1);
                    break;
                case Cursor.FIELD_TYPE_INTEGER:
                    nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).longValue());
                    break;
                case Cursor.FIELD_TYPE_FLOAT:
                    nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).doubleValue());
                    break;
                case Cursor.FIELD_TYPE_BLOB:
                    nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
                    break;
                case Cursor.FIELD_TYPE_STRING:
                default:
                    if (arg instanceof Boolean) {
                        // Provide compatibility with legacy applications which may pass
                        // Boolean values in bind args.
                        nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                                ((Boolean)arg).booleanValue() ? 1 : 0);
                    } else {
                        nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
                    }
                    break;
            }
        }
    }

ここではfor文の中でなんぞ判定して、数値ならNumberにキャストしてからlongValueなりdoubleValueを呼び出してる。これならIntegerでもいけるっぽい。
ところでこのDatabaseUtils.getTypeOfObjectって何だ。

DatabaseUtils.java

    public static int getTypeOfObject(Object obj) {
        if (obj == null) {
            return Cursor.FIELD_TYPE_NULL;
        } else if (obj instanceof byte[]) {
            return Cursor.FIELD_TYPE_BLOB;
        } else if (obj instanceof Float || obj instanceof Double) {
            return Cursor.FIELD_TYPE_FLOAT;
        } else if (obj instanceof Long || obj instanceof Integer
                || obj instanceof Short || obj instanceof Byte) {
            return Cursor.FIELD_TYPE_INTEGER;
        } else {
            return Cursor.FIELD_TYPE_STRING;
        }
    }

このように、実際にはここでinstanceofを使って型を判別しております。見たところIntegerでもShortでもFloatでも良さそうですね。まあFloatをintValueで変換とかはしてくれなさそうですが、おおむね問題は起きないようにしてあるっぽいです。

で、マッチしなかったらStringとみなされて、さっきのbindArgumentsに戻ってみると、そこでさらにBooleanかどうかの判定が行われて、trueなら1、falseなら0、BooleanじゃなければObject#toStringが呼ばれる、と。
これ、このswitch文は必要なのかな。DatabaseUtils.getTypeOfObjectは消して、このswitch文と同化してしまえばいい気がするけどどうなんだろう。getTypeOfObjectがどこかの使い回しだったりするのかな。まあするんだろうなUtilsだし。

最後にtoStringしてるってことは、enumにtoStringを実装しておいてぶっ込むとかもできるし、細かく楽ができそうですね。