デザインとロジックを分離したかった・・・

これは 【その1】ドリコム Advent Calendar 2015 - Adventar の8日目です。

7日目は すべてがKになる (@ka_nipan) | Twitter さんによる ドリコムを支えるデータ分析基盤がTD+AWSに移行した話 - かにぱんのなく頃に です。

【その2】ドリコム Advent Calendar 2015 - Adventar もあります。

f:id:sazae657:20151206210029p:plain

自己紹介

Fz (@sazae657) | Twitter

配線工や商業作曲家等を経て、現在はドリコムにてクロスプラットフォーム開発向けのミドルウエアやデータプロテクション等、低レイヤーの研究開発をしています。

ドリコム釣り部CFO(Chief Fishing Officer)

本日の話

私が手元で使うツールの爆速開発を支えているのがご存じMotifですが、その開発効率化に取り組んでみた話しをゆる〜くお話しします

Motifと採用の経緯に関しては去年のエントリーを参照して頂ければと思います。
ドリコムの俺を支えるUIツールキット - sazae657

MrmとUIL

GUIツールを作る際、コードでベタベタとUIを書くと各種生成、属性や座標の調整、各種解放やらのコードがどんどん増えるのはWin32 APIやCarbonで書いた事がある方ならわかって頂けると思います(おもいたい!!)
そこで、生成や外観の処理を隠蔽してしてロジックの実装に集中させてくれるDCCツールや各種フレームワークが数多くリリースされています。 いくらAPIがシンプルかつ洗練されているMotifとてUIの要素が増えてくるとその傾向は看過できない量になって、爆速開発とモチベーションに暗い影を落とし始めます。

そこで Mrm の登場です。

Mrm (Motif Resource Manager) と UIL (User Interface Language) がMotif アプリケーションにデザインとロジックの分離をもたらしてくれます

Mrmの歴史はかなり長く、1992年にリリースされた OSF/Motif 1.2に[digital]によって実装されたという所までは掴めましたが、資料がほとんど無い上にその頃は小学生だったのでリリース当時の状況等は把握出来ておりません

Mrm+UIL では UIL に外観を定義してCソースにイベント処理等のロジックを書きます。

ウイジェットの生成、各種登録処理はUILを基にMrmが面倒を見てくれます。

猛烈に大雑把に書くとこのような感じになります。

f:id:sazae657:20151208113650p:plain

UILの記法はDCEを書いたことがあればお馴染みの記法です

module モジュール名

 セクション 名前 : 型  {
      (セクション内記述)
 };

end module;

文字列付きラベルウイジェット(XmLabel)を定義するとこのような記述になります

object my_label : XmLabel  {
    arguments {
         XmNlabelString = compound_string("2015年11月26日 (木)発売!!!");
    };
};

これをCで書くとこのようになります

Widget my_label;
Arg arg[1];
XmString str;

str = XmStringCreateLtoR(
             "2015年11月26日 (木)発売!!!", XmSTRING_DEFAULT_CHARSET);
XtSetArg(arg[0], XmNlabelString, str);

my_label = XmCreateLabel(parent ,"my_label", arg, 1);
XtManageChild(my_label);

UIL 素敵 !!!

作ってみる

手元の環境は以下です

ウインドウマネージャーはtwmを愛用しています。
f:id:sazae657:20151208120058p:plain 右端のタマネギ模様のリサイズコーナーとドラッグ時に安心感のある滑り止付きタイトルバーが最高です。

では、このようなアプリケーションをUILで書いてみます

ウインドウにテキストラベルが貼り付いていて、そこに文字が出るだけです

f:id:sazae657:20151207210612p:plain

まずは UIL エンコーディングEUC-JP厳守!!※

halo.uil

module halo

object
    halo_world : XmRowColumn {
        controls {
            label : XmLabel {
                arguments {
                    XmNlabelString =
                        compound_string('右隣の部屋にはテレビが2台',separate=true) &
                        compound_string('左隣の部屋にはスライスたくあんが1kgあるとする',separate=true) &
                        compound_string('顧問がPCを1台着服したと仮定して、現在の情報処理部のPC台数を求めよ');
                };
            };
        };
    };
end module;

次に対応するCソースを書きます エンコーディングEUC-JP厳守!!※

chief.c

#include <stdlib.h>
#include <Xm/XmAll.h>
#include <Mrm/MrmPublic.h>

static char *fallback_resource[] = {
    "*fontList:  -misc-fixed-medium-r-normal--14-*-*-*-*-*-*-*:",
    "*title: HALO WORLD!!",
    NULL
};

int
main(argc, argv)
    int argc;
    char **argv;
{
    XtAppContext app_context;
    Widget topLevel, app;
    Arg al[20];
    int ac;
    /*uilコンパイラーでコンパイルしたファイルを列挙 */
    static String uid_files[] = { "halo.uid" };
    MrmHierarchy  hierarchy_id;
    MrmCode        class ;

    /* お約束(1) */
    XtSetLanguageProc( NULL, (XtLanguageProc)NULL, NULL);

    /* Mrmの初期化 */
    MrmInitialize();

    /* お約束(2) */
    ac = 0;
    topLevel = XtAppInitialize(&app_context, "halo", NULL, 0,
            &argc, argv, fallback_resource , al, ac);

    /* UIDから MrmHierarchyを作る */
    MrmOpenHierarchyPerDisplay(XtDisplay(topLevel),
            (MrmCount) XtNumber(uid_files),
            uid_files,
            (MrmOsOpenParamPtr*)NULL,
            &hierarchy_id);

    /* halo_world を取得 */
    MrmFetchWidget (hierarchy_id,
            "halo_world", topLevel, &app, &class);

    /*  halo_world を表示 */
    XtManageChild(app);
    XtRealizeWidget(topLevel);
    XtAppMainLoop(app_context);

    return 0;
}

uil をコンパイルして uid ファイルを生成

% uil halo.uil -o halo.uid

C ソースをコンパイル & リンク

% cc -g  -I/usr/dt/include -c  chief.c -o chief.o
% cc -g  -o svchost.exe chief.o -L/usr/dt/lib64 -lXm -lMrm -lXt

実行すると EUC-JP環境でない場合はロケールEUC-JPに!!※

% env LANG=ja_JP.eucJP ./svchost.exe

f:id:sazae657:20151206213641p:plain

あら簡単!

UTF-8で使ってみたかった・・・

このuilをUTF-8で保存してコンパイルすると無慈悲なメッセージを吐いてエラーに終わります。
コメント行も許されません。

f:id:sazae657:20151207095743p:plain

UILパーサーのコメントを見ればわかりますが、最終更新は[digital]による1997年です。
UTF-8に対応してたらオーパーツです。

/* 
 * HISTORY
*/ 
#ifdef REV_INFO
#ifndef lint
static char rcsid[] = "$TOG: UilLexAna.c /main/14 1997/03/12 15:10:52 dbl $"
#endif
#endif

/*
*  (c) Copyright 1989, 1990, DIGITAL EQUIPMENT CORPORATION, MAYNARD, MASS. */
/*

という事でUIにマルチバイト文字を使いたい場合、以下のような選択肢があります

  1. EUCで書いてEUCで使う
  2. UILでUIの定義だけして文字列はXのリソースに追い出す
  3. UILを使わないでCソースにベッタベッタとXmCreateXX関数を書く

という感じになります・・・

でも、やっぱり・・・

ですが!

折角Motifのソースが公開されているのに妥協するのは面白くないので、自分で使うツールを手早く手間無く見た目は格好良く(Motif)爆速開発する目的の為にゴリ押しでUILを何とかしてみます。

神聖なMotifのソースに手を入れるのは非常に気が引けますが、液晶を指でみょんみょんする人の気持ちになって思い切ってやってみましょう。

まず、OpenMotifのUILコンパイラーのソースを読んでみます

問題の箇所が1145行目のココです

 switch (z_cell.backup)    /* backup holds special processing code */
 {
case control_char: /* encountered a control char in a string or
        * comment - issue a diagnotic and continue */
    issue_control_char_diagnostic( c_char );
break;

Motif / Code / [9f0120] /clients/uil/UilLexAna.c

ここで d_control_char が発生して無慈悲なエラーを吐いています

ココをコメントアウトしたバイナリーを作って、冒頭のUILをコンパイルしてみます

f:id:sazae657:20151208084903p:plain

あ・・・
予想通り、ゴッソリ削れてしまいました。

次にエラー診断をしている箇所に飛びます

severity = diag_rz_msg_table[ message_number ].l_severity;

http://sourceforge.net/p/motif/code/ci/master/tree/clients/uil/UilDiags.c#l227

diag_rz_msg_table[d_control_char] の l_severityでエラーレベルが定義されています d_control_char = 9ですので、この定義を見ると

typedef struct
{
  XmConst int  l_severity;
  char XmConst *ac_text;
} diag_rz_msg_entry;

XmConst diag_rz_msg_entry diag_rz_msg_table[81] =
(略)
 { 3, msg9 },

Motif / Code / [9f0120] /clients/uil/UilMessTab.h

d_control_char の l_severity は 3 となっているので、展開すると以下のようになります

 { 3,  "unprintable character \\%d\\ ignored" }

l_severity = 3の定数を見てみます

#define Uil_k_error_status    3

Motif / Code / [9f0120] /clients/uil/Uil.h

d_control_char の severity は Uil_k_error_status という事になっていそうです
UilDiags.c の診断ルーチン では Uil_message_count[Uil_k_error_status] の件数をインクリメントしています
もうダメそうな匂いが漂ってきました・・・

Uil_message_count[ severity ]++;

流れ流れてエントリーポイントに戻ってきました Uil_message_count[Uil_k_error_status] はUilDiags.cでインクリメント済みで明らかにゼロではないのでチェックメイトです。

    /* If there are any error/severe messages, then don't return */
    /* a symbol table for the callable compiler - clean up here */
    if ( Uil_message_count[Uil_k_error_status]>0 ||
     Uil_message_count[Uil_k_severe_status]>0 ) {

Motif / Code / [9f0120] /clients/uil/UilMain.c

という事で・・・
最初に上げた switch 文で z_cell.backup が control_char になった状態を見てみます

わかりやすいように、UILの文字列は compound_string('ゆずこしょう'); に変更しました

(gdb) p/x z_cell
$2 = {action = 0x4, next_state = 0x8, backup = 0x1, unused = 0x0}

z_cellを作っている場所を探します

l_class = class_table[ c_char ];     /* determine its class */
z_cell = token_table[ l_state][l_class ];   /* load state cell */

Motif / Code / [9f0120] /clients/uil/UilLexAna.c

関連変数を見ていきます

(gdb) p/x c_char
$3 = 0x82
(gdb) p/x l_state
$4 = 0x8
(gdb) p/x l_class
$5 = 0x10

class_table[c_char = 130] とtoken_table[l_state = 8][l_class = 16]を見てみます

static cell XmConst token_table[ max_state+1][ max_class+1] = {
(中略)
/* class_illegal */ { special,      state_str_1,    control_char },

static char class_table[ 256 ] = {
(中略)
/* 80 */    class_illegal,  class_illegal,  class_illegal,  class_illegal,

Motif / Code / [9f0120] /clients/uil/UilLexAna.c

つまるところ
「ゆ」(0xE3 0x82 0x86) は0x82から文字コードとして class_illegal 扱いになって、制御文字(control_char)として片付けられていました。

ここまで来たら・・・

事象は掴めたので即行動してみました。

Motif は UTF-8に対応しているので、UIL コンパイラーに対して文字判定で class_illegal+control_charを喰らったらUTF-8判定を掛けて、処理を強行する簡単な改造を実施してみました

(デカいのでgistに貼りました)
UILにUTF-8を無理矢理通すゴリ押し · GitHub

手元で使えりゃいーじゃんレベルですので厳密な検証はしていません、ご了承ください

f:id:sazae657:20151207141007p:plain

ちゃんと表示された!!!

UILを使いまくってデザインとロジックを分離しよう!

ゴリ押し改造の結果、遂に念願のUTF-8のソースがコンパイルできるようになったので使いまくってみます!

折角ですので、Motifで超絶面倒くさいと評判のメニューバーとプルダウンメニューを扱ってみます。
C側のコードだけで書くとXmCreateXXの嵐で大変な事になるのですが、UILで書けば定義するだけでコードの変更は必要ありません

冒頭のコードにメニューバーの定義を追加して実行してみます

object
    menu_bar : XmMenuBar {
        controls {
            XmCascadeButton main_menu;
        };
    };

object
    main_menu : XmCascadeButton {
        controls {
            XmPulldownMenu {
                controls {
                    member1 : XmPushButton {
                        arguments {
                            XmNlabelString = compound_string("紫");
                        };
                    };
                    member2 : XmPushButton {
                        arguments {
                            XmNlabelString = compound_string("ピンク");
                        };
                    };
                    member3 : XmPushButton {
                        arguments {
                            XmNlabelString = compound_string("明るい茶色");
                        };
                    };
                };
            };
        };
        arguments {
            XmNlabelString = compound_string("情報処理部");
        };
    };

f:id:sazae657:20151207213836p:plain

あら簡単!

次はメニューが選択された時の処理を書いてみます

イベント駆動ですのでコールバックを登録します。

まず、UILにイベントと呼んで欲しい関数を宣言します。

procedure
    member_select();

次に呼んで欲しいウイジェットの呼んで欲しいイベントに定義したコールバックを設定します 今回はメニュー項目が選択されたら member_select() というコールバックを呼んでもらいます

member1 : XmPushButton {
    arguments {
            XmNlabelString = compound_string("紫");
        };
        callbacks {
            XmNactivateCallback = procedure member_select();
        };
};

あとはSystem.Reflectionで・・・という訳には行かないのでCのコードに定義を追加します。

呼び出し元のラベルの文字列を取得してコンソールに出力するコードを書いてみます

/* member_select コールバック */
static void
member_select(widget, call_data, client_data)
    Widget    widget;
    XtPointer client_data;
    XtPointer call_data;
{
    XmString str;
    char* cstr;
    Arg ar[1];

    /* 呼び出し元のラベルの文字列を取得してみる */
    XtSetArg(ar[0], XmNlabelString, (XtArgVal)&str);
    XtGetValues(widget, ar, 1);

    /* C文字列へ変換 */
    cstr = (char *)XmStringUnparse(str, NULL, XmCHARSET_TEXT, XmCHARSET_TEXT, NULL, 0, XmOUTPUT_ALL);

    /* コンソールに出してみる */
    fprintf(stderr, "SELECT: %s\n", cstr);

    /* 後始末 */
    XtFree(cstr);
    XmStringFree(str);
}

つぎに MRMに登録する為のリストを定義します。
key - valueで列挙していく形です (スクリプト言語の拡張モジュールを書いたことがあればお馴染みですね)

static MrmRegisterArg  register_args[] = {
    {"member_select", (XtPointer) member_select }
};

これで member_select() を member_select という名前でマッピングしました

最後に MRMへコールバックを登録します。

Hierarchyへ登録するタイプとグローバルへ登録する2タイプがありますが、今回はHierarchyへ登録します。

    /* Hierarchy へコールバックの登録 */
    MrmRegisterNamesInHierarchy (hierarchy_id,
          register_args, XtNumber(register_args));

あとは実行するだけです

f:id:sazae657:20151207222821p:plain

あら簡単!

後ろのxtermはUTF-8ですので、ちゃんとウイジェットからUTF-8で取れています。

お前すごいよMotif!現代でも全然通用するよ!めっちゃ古いのに!

参考資料など

20年ほど前は解説書籍やベンダー製のリファレンスなどが出版されていたようですが、絶版です。

OpenMotifのソースコードとman位しか現存していません。。。。

まとめ

Motif はかっこいい
Motif はモダン
Motif 最高
ありがとう[digital]

この記事は CDE 環境を構築した openSUSE にて Visual Studio Codeで書きました

f:id:sazae657:20151208193945p:plain The Common Desktop Environment


9日目は hang chang - Adventarさんによる 今さら聞けない、はじめてのOGP設定 - Funny panic!です