ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

Tcl/Tkで使われているカプセル化手法

Tcl/Tkのソースコードを眺めていたら、面白そうなコードを見かけたので紹介します。

tclとtkの関係

例えば以下のコードをhello.tcl等に保存して、wish hello.tclを実行すると、いわいるtcl/tkのアプリケーションが立ち上がります。

button .b -text {push} -command {
    tk_messageBox -message {hello}
}
pack .b 

拡張子tclや、中身のコードがTclの文法に沿って書かれている事から、なんとなくTclのインタープリターがTkのwidgetsを読んでいるように見えます。ところが実際には最初に起動するのはTkのプログラムで、Tkのmain/WinMain関数がTclのインタープリターを呼び出しています。Tkを使わない時はtclshを呼び出し、Tkを使うときはwishを呼び出すのはそういう理由です。

Tclのインタープリタは、wishの中で静的にリンクされ、wishからは普通の関数のようにインタープリターを呼び出しています。普通はTclとTkは同時にインストールされるので気がつきませんが、Tclはtclshのmain関数を持ち、Tkはwishのmain関数を持ちます。ソースツリーは分かれていますが、Tk(wish)をビルドするときには、Tclのソースも必要になります。

ここではTclの方をインタープリタ内部、wishの方をアプリケーションと呼びます。

Tcl インタープリター構造体

Tclインタープリタとアプリケーションは、一つの構造体を共有してデータのやりとりをします。構造体は別々のヘッダーファイルに定義されています。

インタープリター内部で使用する構造体Interpは、tcl8.5.10/generic/tclInt.hに定義されています。

typedef struct Interp {
    /*
     * Note: the first three fields must match exactly the fields in a
     * Tcl_Interp struct (see tcl.h). If you change one, be sure to change the
     * other.
     *
     * The interpreter's result is held in both the string and the
     * objResultPtr fields. These fields hold, respectively, the result's
     * string or object value. The interpreter's result is always in the
     * result field if that is non-empty, otherwise it is in objResultPtr.
     * The two fields are kept consistent unless some C code sets
     * interp->result directly. Programs should not access result and
     * objResultPtr directly; instead, they should always get and set the
     * result using procedures such as Tcl_SetObjResult, Tcl_GetObjResult, and
     * Tcl_GetStringResult. See the SetResult man page for details.
     */

    char *result;		/* If the last command returned a string
				 * result, this points to it. Should not be
				 * accessed directly; see comment above. */
    Tcl_FreeProc *freeProc;	/* Zero means a string result is statically
				 * allocated. TCL_DYNAMIC means string result
				 * was allocated with ckalloc and should be
				 * freed with ckfree. Other values give
				 * address of procedure to invoke to free the
				 * string result. Tcl_Eval must free it before
				 * executing next command. */
    int errorLine;		/* When TCL_ERROR is returned, this gives the
				 * line number in the command where the error
				 * occurred (1 means first line). */
    struct TclStubs *stubTable;
				/* Pointer to the exported Tcl stub table. On
				 * previous versions of Tcl this is a pointer
				 * to the objResultPtr or a pointer to a
				 * buckets array in a hash table. We therefore
				 * have to do some careful checking before we
				 * can use this. */

    TclHandle handle;		/* Handle used to keep track of when this
				 * interp is deleted. */

    Namespace *globalNsPtr;	/* The interpreter's global namespace. */
    Tcl_HashTable *hiddenCmdTablePtr;
				/* Hash table used by tclBasic.c to keep track
				 * of hidden commands on a per-interp
				 * basis. */
    ClientData interpInfo;	/* Information used by tclInterp.c to keep
				 * track of master/slave interps on a
				 * per-interp basis. */
    Tcl_HashTable unused2;	/* No longer used (was mathFuncTable) */

    /*
     * Information related to procedures and variables. See tclProc.c and
     * tclVar.c for usage.
     */

    int numLevels;		/* Keeps track of how many nested calls to
				 * Tcl_Eval are in progress for this
				 * interpreter. It's used to delay deletion of
				 * the table until all Tcl_Eval invocations
				 * are completed. */
    int maxNestingDepth;	/* If numLevels exceeds this value then Tcl
				 * assumes that infinite recursion has
				 * occurred and it generates an error. */
    CallFrame *framePtr;	/* Points to top-most in stack of all nested
				 * procedure invocations. */
    CallFrame *varFramePtr;	/* Points to the call frame whose variables
				 * are currently in use (same as framePtr
				 * unless an "uplevel" command is
				 * executing). */
    ActiveVarTrace *activeVarTracePtr;
				/* First in list of active traces for interp,
				 * or NULL if no active traces. */
    int returnCode;		/* [return -code] parameter. */
    CallFrame *rootFramePtr;	/* Global frame pointer for this
				 * interpreter. */
    Namespace *lookupNsPtr;	/* Namespace to use ONLY on the next
				 * TCL_EVAL_INVOKE call to Tcl_EvalObjv. */
    /* 長いので省略 */
} Interp;

アプリケーションから見た構造体はTcl_Interpは tcl8.5.10/generic/tcl.hで定義されています。

typedef struct Tcl_Interp {
    char *result;		/* If the last command returned a string
				 * result, this points to it. */
    void (*freeProc) _ANSI_ARGS_((char *blockPtr));
				/* Zero means the string result is statically
				 * allocated. TCL_DYNAMIC means it was
				 * allocated with ckalloc and should be freed
				 * with ckfree. Other values give the address
				 * of function to invoke to free the result.
				 * Tcl_Eval must free it before executing next
				 * command. */
    int errorLine;		/* When TCL_ERROR is returned, this gives the
				 * line number within the command where the
				 * error occurred (1 if first line). */
} Tcl_Interp;

勘の良い人は気がつくでしょうが、アプリケーションから見た構造体Tcl_Interp は、Tclインタープリタで使用している構造体 Interp の上から3つのメンバーだけを定義した物です。

Tclインタープリターへのアクセス方法

アプリケーションで使用するTclインタープリターは、Tcl_CreateInterp()関数で生成します。この関数は、Tcl_Interp へのポインタを返しますが、関数内部で生成しているのは、 Interp 構造体です。Tclインタープリターは Interp へのポインタを渡しているのですが、アプリケーション側では Tcl_Interp へのポインタと見えるため、上の3つのメンバー変数にアクセス出来ます。


図で書くとこうですね。
別々のヘッダーファイルを使う事で、Tclインタープリターの内部を隠蔽しています。

問題点

ソースのコメントにもあるとおり、同じ事を2つのファイルに書いています。この2つのファイルの内容がずれたときに、コンパイルは通るけど実行時にcoreを出す状況が簡単に作れてしまいます。また、今はポインタ、ポインタ、int なのでたまたま問題ないですが、charのようなサイズの違う変数が入ってくると、最適化によってメンバ変数へのオフセットが変わってくる可能性があります。Cの規格では構造体のメンバーの配置に関して、先頭のアドレスとメンバーの順番については記載がありますが、いわいるパディングについては記載がありません。つまり構造体のサイズを小さくするためにメンバー変数を詰めて配置しても良いし、速度を重視するために32bitや64bitの単位にアラインメントしても良いのです。そうなると、例えばTcl側のソースコードだけ最適化をかけた時、コンパイル、リンクとも正常終了するのに、実行時にcoreを出して落ちる可能性がでてきます。誰かが魔が差して #pragma pack 等と言い出すと一気に破綻するでしょう。

解決策としては、アプリケーション側にはInterpのポインタだけを定義し、メンバー変数へのアクセスに関してもアクセス用の関数の定義だけを公開する方法があります。そうすれば同じように構造体のメンバへのアクセス制限ができると同時に、上のような問題は発生しなくなります。