tcl7.xjp と tcl8.0jp の非互換およびスクリプト再コンパイルに伴う副作用 m-hirano@sra.co.jp tcl8.0 の On the fly bytecode compile 機能のために、tcl7.xjp と tcl8.0jp の間には、internalCode への漢字コード自動変換機能回りに非互換 が生じました。また、eval, uplevel, source 等の、実行中にスクリプトの再 コンパイルを引き起こすコマンドの使用も internalCode への漢字コード自動 変換機能により有害な副作用を生じさせることがあります。 このドキュメントはその非互換部分と副作用について説明するものです。読 者が tcl7.xjp の kanji internalCode コマンドの使用法と振舞に関してある 程度知識を持っていることを前提としています。 ちなみに、漢字コードの違いを意識し、tcl7.xjp の internalCode への漢 字コード自動変換機能に頼らないスクリプトを書いている方々には、非互換部 分はあまり気にならないでしょう。 1. 非互換部分 1.1. 症例 以下のようなスクリプトがあるとします。 1 kanji internalCode JIS 2 set jis あいうえお 3 kanji internalCode SJIS 4 set sjis あいうえお 5 kanji internalCode EUC 6 set euc あいうえお 7 foreach i "$jis $sjis $euc" { 8 puts [ kanji code $i ] 9 } 10 set l [ list $jis $sjis $euc ] 11 12 kanji internalCode EUC 13 foreach i "JIS SJIS EUC" { 14 kanji internalCode $i 15 set idx [ kanji lsearch $l あ* ] 16 puts $idx 17 } tcl7.xjp で上記を実行すると、インタラクティブモード、非インタラクティ ブモードともに、 JIS SJIS EUC 0 1 2 の出力が得られます。tcl8.0jp で実行すると、インタラクティブモードで は、 JIS SJIS EUC 2 2 2 非インタラクティブモードでは、 EUC EUC EUC 0 0 0 となります(SunOS 4.1.x の場合)。 1.2. 説明 tcl8.0jp でなぜこのようなことが起きるかというと、 漢字を含む即値がコンパイルされる時には、現在の internalCode へ コード変換される。その後、再コンパイルが起きない限り、コンパイ ルされた即値はコード変換されない。 という実装にしたからです(立ち上げ時のデフォルト。2.2. で後述する方法 でコンパイル時の internalCode へのコード変換を完全に禁止できます)。 # internalCode の振舞に関して tcl7.xjp との互換を保つには、バイトコー # ド実行部分に手を入れなければならなくて(多分本当)、これがめんどくさい、 # かつ実行パフォーマンスが落ちるのが目に見えているからこういう仕様に # した、というのが本音です。 tcl8.0jp でのインタラクティブモードと非インタラクティブモードでの結 果の違いは、この実装と、以下の理由で生じます。 非インタラクティブモードでは、スクリプトファイルを最初に全部コンパイ ルした後で、バイトコードの実行を開始します(tcl8.0 の元々の実装)。した がって、 kanji internalCode XXX の記述があっても、コンパイルはされるが、 当然直ちに実行されないので、コンパイル時には internalCode は変 化しない。 ことになります。 一方インタラクティブモードでは、1 行読み込む毎にコンパイルし、バイト コードを実行します(これも tcl8.0 の元々の実装)。したがって、 kanji internalCode XXX のコンパイル・実行の後の漢字を含む即値 のコンパイルには、設定した internalCode が直ちに効力を発揮する。 ことになります。このような internalCode の変化のタイミングが、インタ ラクティブモードと非インタラクティブモードの結果の違いを生み出している わけです。 1.3. 解決法:eval を使う internalCode を設定した後に、漢字を含む即値を再コンパイルさせる、つ まり、 漢字を含む即値を持つコマンドを、eval コマンドを介して実行させる。 ことで tcl7.xjp と互換になります。tcl8.0 の実装では、eval コマンドは 渡された引数を再コンパイル・実行します。 実際に上記スクリプトを書換えると、 1 kanji internalCode JIS 2 eval set jis あいうえお 3 kanji internalCode SJIS 4 eval set sjis あいうえお 5 kanji internalCode EUC 6 eval set euc あいうえお 7 foreach i "$jis $sjis $euc" { 8 puts [ kanji code $i ] 9 } 10 set l [ list $jis $sjis $euc ] 11 12 kanji internalCode EUC 13 foreach i "JIS SJIS EUC" { 14 kanji internalCode $i 15 set idx [ eval kanji lsearch {$l} あ* ] 16 puts $idx 17 } のようになります。 2. 副作用 1.3. では eval によるスクリプトの再コンパイルで積極的に internalCode へのコード変換を活用しました。しかし、internalCode へのコード変換が有 害な副作用を生むこともあります。 2.1. 症例 あるプロシジャ proc Foo { x } { .... } が、unknown プロシジャで auto_load されるように tclIndex を記述して いたとします。また、スクリプト内で最初に登場するの Foo の用法が、 Foo \x95\x5c のように、バックスラッシュエスケープで記述した即値が、たまたま漢字コー ドとして正しいシーケンス(この例では SJIS で '表' の文字になる)になるよ うなものだったとします。 \x95\x5c というバックスラッシュエスケープは、"Foo \x95\x5c" という文 字列のコンパイル時に、内部的にバイナリ、つまり SJIS の '表' に置換され ます(tcl8.0 の元々の実装)。 さて、"Foo \x95\x5c" の呼び出しは unknown プロシジャで行われます。実 際には proc Foo { x } { .... } を含むファイルを source し、uplevel コ マンドで "Foo \x95\x5c" を呼び出します。 このとき uplevel コマンドは "Foo 表" という文字列をコンパイルするこ とになります。なぜなら、すでに \x95\x5c は "Foo \x95\x5c" という文字列 のコンパイル時に SJIS の '表' に置換されているので、"Foo 表" が unknown プロシジャの引数として使われるからです。 このコンパイルにより、tcl8.0jp は通常の SJIS の '表' と解釈し、 internalCode へのコード変換を行ってしまいます。 もしこの時の internalCode が SJIS もしくは ANY だった場合は問題はな いのですが、例えば SunOS 4.1.x のデフォルトである EUC であったとしましょ う。この場合は結果的に \x95\x5c が EUC の '表' である \xc9\xbd に変換 されて Foo の引数に渡されることになります。しかも、この unknown プロシ ジャによる uplevel コマンドでのコンパイルは、一番最初の Foo の呼び出し の時にしか起こりませんから、 Foo \x95\x5c Foo \x95\x5c のようなスクリプトがあると、 Foo \xc9\xbd Foo \x95\x5c が実行されてしまい、同じ値を Foo の引数に使用しても、最初の呼び出し とそれ以降の呼び出しとで結果が異なるという、大変奇妙な状況に陥ります。 2.2. 解決法:コンパイル時のコード変換の禁止 この問題を回避するために、現状の tcl8.0jp の 実装では、 $tcl_library/init.tcl で unknown プロシジャを置き換えて、unknown プロ シジャが発生させるコンパイルの時には internalCode へのコード変換を禁止 するようにしています。 source, uplevel, eval (その他のコマンドでも起きるかも知れませんが、 ソースコード以外のドキュメントが無いので未調査)等によるコンパイル時の internalCode へのコード変換を禁止するには、 kanji convertWhenCompile no を、コンパイルによる internalCode へのコード変換が起きて欲しくない場 所に挿入します。 kanji convertWhenCompile yes とすることで、コンパイル時の internalCode へのコード変換を可能にでき ます。 これらを適切に使い分けることで、有害な副作用に対応できます。 # tcl8.0 のオリジナルコードのパーサ/バイトコード生成部分を適切に書換え # ることでも対応出来るはずですが、変更部分が多そうなので止めてます ^^; # ま、「バイトコード生成のパフォーマンスとの trade off を考えて」とい # うもっともらしい理由を一応付けておきましょう B) また、もうお気づきでしょうが、漢字を含んだ変数名、プロシジャ名等も、 eval 等の再コンパイルによる internalCode へのコード変換によって影響を 受けます。漢字を含んだ関数名、変数名を使っている時に、あまり派手に internalCode を切替えると、思わぬ苦労をすることがあるでしょう。 3. 漢字文字列パージングの完全な禁止 場合によっては、全てのデータを漢字として扱って欲しくない場合がありま す。特に、いわゆるバイナリデータに regsub, subst を使って複雑な文字列 変換を行い、それを eval へ渡すような場合です。 このような場合、 kanji scanKanjiToken no とすることで実現できます。立ち上げ時のデフォルトは kanji scanKanjiToken yes になっています。必要に応じて切替えて使用して下さい。 通常デフォルトのままで大丈夫ですが、特に海外製のスクリプトを使用して いる場合に subst で error が出たりするのなら、切替えてみると効果がある かもしれません。 scanKanjiToken が no の場合、tcl8.0jp のパーサ部分はほぼ完全に tcl8.0 と互換になります。つまり、日本語に関してパーサは何もしなくなり ます。特に JIS コードを扱うスクリプトは壊滅状態になるでしょう。 ちなみに現在の仕様では、convertWhenCompile はインタープリタ(interp コマンドで生成されたインタープリタ)毎に別ですが、scanKanjiToken はプロ セス全体に波及します。 4. 漢字文字列と '\\' エスケープ tcl7.xjp では '\\' 直後に漢字文字列ある場合の動作が明確に決められて いませんでした。これを Tcl/Tk 8.0.4jp 1.4a で決めました。以下のように 置換されます。 \漢字 -> 漢字 "\漢字" -> 漢字 {\漢字} -> \漢字 "{\漢字}" -> {漢字} このように、ASCII 文字で '\\' エスケープされないものと同様の動きをし ます。 また、2.1. でも述べた、何らかの正しい漢字文字列に見える \xXX\xXX や \XXX\XXX (X は [a-fA-F0-9]) のシーケンスがある場合、これらは internalCode に変換されません。以下に SJIS の '表' となる \x95\x5c を 例に挙げて説明します。internalCode は EUC であるとします。 \x95\x5c -> internalCode にかかわらず、常に SJIS の '表' a. "JISの漢字 \x95\x5c" -> 'JISの漢字' のみが internalCode に変換され、 \x95\x5c は常に SJIS の '表' つまり、 'JISの漢字 表' .... 一文字列内に EUC と SJIS のコードが混在。 b. {JISの漢字 \x95\x5c} -> 'JISの漢字' のみが internalCode に変換され、 \x95\x5c は Tcl の {} による quote 規則にのっとり、 'JISの漢字 \x95\x5c' ... EUC 文字列 c. "{JISの漢字 \x95\x5c}" -> a. 同様、'JISの漢字' のみが internalCode に変換 され、\x95\x5c は常に SJIS の '表' つまり、 '{JISの漢字 表}' ....一文字列内に EUC と SJIS のコードが混在。 当然ですが、文字コードを混在させる場合は注意して下さい。 5. SJIS 半角カナサポート Tcl/Tk 8.0.4jp 1.3a より、Windows プラットフォームのサポートが追加さ れました。これにより、8.0jp で無視し続けていた半角カナをある程度サポー トすることにしました。 tcl8.0jp で半角カナの使用できる漢字コードは SJIS に限ります。 8.0.4jp 1.3a からは、漢字コードの自動判別において、8.0.4jp 1.2 以前 のようにほぼ常に EUC と見なすのではなく、JIS/EUC/SJIS の internal code の他に、コード自動判別困難な場合にユーザが SJIS/EUC のどちらを望むのか の指定が可能になりました。 kanji preferSjis yes とすると、EUC/SJIS 判別が困難な場合、常に SJIS と見なすようになりま す。これにより、特に SJIS 半角カナが恩恵を受けます。逆に言うと EUC が 壊滅します。 kanji preferSjis no とすると、これまでの tcl8.0jp とほぼ同様、EUC と見なすようになります。 UNIX プラットフォームでは default で no で、ほぼ tcl8.0.4jp 1.2 以前 のバージョンと互換があります(ほぼ、というのは、若干 SJIS と判断するケー スを増やしたからです)。Windows プラットフォームでは default で yes で す。また、当然ですが、Windows プラットフォームでの default の internal code は SJIS です。 したがって、Windows プラットフォームで EUC で書かれたスクリプトを処 理する場合などは気を付けて下さい。8 bit 文字列はほぼ確実に SJIS だと見 なされ、byte compile error が出ます。 ちなみに、JIS なら確実に自動コード判定できるので、マルチプラットフォー ムで動かすスクリプトの漢字コードとして、安心して使用できるはずです。 6. Windows プラットフォームでの漢字コードの扱い Win32 API は SJIS 文字列を引数として使えます。ファイル名、フォント名、 レジストリキー、タイトルバー、etc.. です。 こういうことをする人が多いとは思えませんが、Windows プラットフォーム で、internal code を SJIS 以外にした場合、Win32 API に漢字文字列を渡そ うとしても、正しく SJIS で渡らなくなる可能性があります。 したがって、internal code を SJIS 以外に変える場合、スクリプト内で Win32 API に渡る文字列は、きちんと SJIS になるようプログラマ自身が気を つけなければなりません。tcl8.0jp はこのための care を全く行いません。 言うまでもないとは思いますが、最悪の場合、ファイルを失ったり、むちゃく ちゃなレジストリキーにむちゃくちゃな値を書き込んでしまう恐れもあります。 7. 立ち上げ時の internal code tcl8.0.4jp1.4 までは、tcl7.xjp 同様、立ち上げ時(default)の internal code は環境変数 LANG を参照して設定していました。tcl8.0.5jp 1.5a より これを変更し、 1. まず環境変数 TCL_KANJICODE を参照。設定する値は、kanji internalCode コマンドの引数と同じ、SJIS|EUC|JIS|ANY である。 プラットフォームが Windows の場合、もし TCL_KANJICODE が設 定されていなければ、SJIS とする。 2. プラットフォームが UNIX ならば、TCL_KANJICODE が設定されて ない場合、さらに LC_ALL LC_CTYPE LANG の順で環境変数を参照。値はいわゆる locale 名。要するに TCL_KANJICODE が設定されてなければ、tcl8.0.4jp 1.x, tcl7.xjp と同様の動作をする。 のようにしました。 TCL_KANJICODE を用いて Windows で internal code を EUC にした場合、 kanji preferSjis は自動的に false になります。