この間、FileHandlerをECCSkeltonに移植してCSHELLクラスの動作確認を行いましたが、今回はTextToSpeechの移植でCVOICEクラスの動作確認を行いました。

 

結果は?

 

動作はASCII版と同じく申し分ないのですが、今までのサンプルでは全く問題が無かったにもかかわらず、バージョンダイアログのSTATICコントロール(SS_ICON)のアイコンとツールバービットマップが表示されない不具合が発生しました。(アイコンは、Explorerのexeファイルには表示されており、読み込まれていると考えられます。)

通常このような場合、

 

(1)リソースコンパイルの失敗→IDは通っているが、実体のリソースが無い。しかし、コンパイルエラーはありません。

(2)リソースID(文字列)の相違→「(間違った)IDが見当たりません」等、通常はエラーが出ます。

(3)リソースIDの文字列と定義整数の不連携→エラーは出ません。

 

「整数IDがASCIIのファイル名を参照していて、それからワイド文字列IDに連携されていないのではないか?」とも考えるのですが、リソースが正常に表示されている他のサンプルと比較しても相違が無いので、少し頭を冷やしてから再度見直します。

 

閑話休題。しかし、今頃気が付いたのですが、コンパイラーをワイド文字対応させてコンパイルしても、

 

#include        "(ファイル名)"

 

はワイド文字の 'L' を付けるとファイル名として識別されず、エラーになりますね。(従ってファイル名表記はASCIIデータのままの筈です。)

 

rcファイルも同様(例↓)ですが、

 

Borland Resource Compiler  Version 5.40
Copyright (c) 1990, 1999 Inprise Corporation.  All rights reserved.


Error TextToSpeech.rc 6 10: Expecting filename

 

更にリソース記述の際の文字列にはLを付けるところ(例↓)、

 

(メニューの場合)

    POPUP L"ファイル(&F)"
    {
        MENUITEM
L"スクリプトの入力(&I)", IDM_INPUT
        MENUITEM
L"クリップボードから入力(&C)", IDM_CLIP
        MENUITEM
L"ファイルから入力(&O)", IDM_OPEN
        MENUITEM
L"スクリプトファイルの保存(&S)", IDM_SAVE
        MENUITEM
LEPARATOR
        MENUITEM
L"終了(&X)", IDM_EXIT
    }

 

(ダイアログの場合)

IDD_VERSION DIALOG DISCARDABLE 0, 0, 160, 64
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION
L"バージョン情報"
FONT 9,
L"Times New Roman"
{
 CONTROL IDI_ICON, 0,
L"STATIC", WS_CHILD | WS_VISIBLE | SS_SUNKEN | SS_ICON, 4, 14, 18, 18
 CONTROL
L"", IDC_VERTXT, L"STATIC", WS_CHILD | WS_VISIBLE | SS_CENTER | SS_SUNKEN, 32, 10, 106, 26
 CONTROL
L"OK", IDOK, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON | BS_CENTER, 66, 46, 26, 12
}

 

アイコンやビットマップなどのファイル指定の際にLを付けると(例↓)

 

IDI_ICON    ICON    DISCARDABLE    L"Icon.ico"
 

 

Borland Resource Compiler  Version 5.40
Copyright (c) 1990, 1999 Inprise Corporation.  All rights reserved.


次のように読めてはいるのですが、
 

Error TextToSpeech.rc 97 27: Cannot open file: "Icon.ico

 

始めのダブルクォテーションが残り、エラーになります。

 

不思議ですね。

Vectorで公開している"BCCFormandBCCSkelton"に、"ECCSkelton"フォールダーを追加して、本日公開しました。(注意:Vectorからダウンロードできる迄、一定お時間がかかります。また、ダウンロードできるURLは↑のものと異なるかもしれませんのでご注意ください。)

 

繰り返しになりますが、これを機会にもう一度BCCFormandBCCSkeltonのパッケージについて説明します。

 

1.背景

BCCForm→2000年にフリーで利用できるようになったBorland(当時) C++ Compiler(bcc551)にリソースエディターがなかった為、2002年に公開した(偶々当時駐在していたシンガポールで取得したbcc550の)BRC32.exeでコンパイルできるBorland Resource Editor準拠の独自リソースエディターです。(これはライブラリーなしで純粋にSKDだけで書かれていましたが、2018年のPC入替の時の事故でソースを失いました。)

 

BCCSkelton→当時Win32 SKDのウィンドウ登録、作成、コールバック関数およびエントリーポイント関数のファイルを書いていましたが、毎回同じコードを書く馬鹿らしさに、自分なりの「Win32 SKD 省力、効率化ライブラリー」を作成し始め、リソース(*.rc)ファイルからウィザード(SkeltonWizard.exe-これはBCCSkeltonで書かれましたが、2018年の事故でソースを失いました)で4本のスケルトンファイルを作成する(IDE迄には届きませんでしたが)ユーティリティを作成しました。ユーティリティにはイメージエディター、ツールバーエディター等を付けてリソース編集面を充実させていました。

 

2.ECCSkelton-"Embarcadero C++ Compiler"の時代のSkelton

ECCSkelton→BCCFormとBCCSkeltonでつくるプログラムは当時(2001年にXPがリリースされるまでは、Unicodeを使わないWindows 98が主流)を反映して文字列は基本的にASCII(Shift-JIS)ベースであり、文字列を使わない限り差はないのですが、矢張りOSがユニコード(UTF-16のワイド文字)をネイティブで使っているので、それにも対応しないとまずいでしょ、ということで「ちょこっと発展型」のBCCSkeltonWplus等を考えましたが、矢張り二本立てには無理があり、文字列を扱う処理なら「UTF-16ワイド文字一本やり」のライブラリーが欲しい、ということで"Embarcadero C++ Compiler"の時代のSkeltonという意味で、BCCSkeltonを一から見直しながら、SkeltonWizardの排出するコードに最小の手直しで対応できるライブラリーを目指しました。

 

3.BCCSkeltonとの相違~BCCSkeltonファイルからの移植

(1)文字列データ

Shitf-JISの文字列データは1バイト(漢字は2バイト)のASCII文字データであり、これらをUTF-16の2(または4)バイト文字データに替える必要があります。

1バイト変数(char、LPSTR、LPCSTR等)は2バイト変数( wchar_t(WCHAR)、LPWSTR、LPCWSTR等)に替えます。

1バイト文字(列)定数(’A'、"abc"、NULL)2バイト文字列定数(L’A'、L"abc"、L'\0')に替えます。

また、Microsoftが"TCHAR型"という「蝙蝠変数」(TCHAR、LPTSTR、TEXT(文字定数)の_T(文字定数) マクロ _TEXT(...)  )を開発し、"UNICODE"フラグが定義されていることを条件に1バイト文字、2バイト文字に自動振り分けを可能にしていますが、ECCSkeltonでは全て「WCHARを基本型」(WCHAR、LPWSTR、L(文字列定数))にしています。(例外的に1バイトデータを扱う(例:CICON.h)以外はcharを使うことはない。)

(2)ワイド文字対応関数

①MicrosoftはWindowsのWin32 APIの関数について、WindowsのUTF-16対応の為に、文字(列)を引数で受け付け、処理し、または出力する関数に関しては1バイト文字用と2バイト文字用の二つを用意しており、「関数名」を"UNICODE"フラグが定義されていることを条件に「関数名W」(TRUE)「関数名A」(FALSE)にマクロ定義しており、UNICODE(ECCSkelton.hで定義している)条件で自動的に振り替えられますが、ECCSkeltonは明示的に(分かる限り)「関数名W」を使用しています。

 

②構造体も同様に"UNICODE"フラグで「構造体W」(TRUE)「構造体A」(FALSE)等に自動的に振り替えていますが、ECCSkeltonでは明示的に(分かる限り)「構造体W」を使用しています。

 

③エントリーポイントは「関数名W」のルールから外れており、

    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                        LPSTR lpCmdLine, int nCmdShow) {
から

    int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                        LPWSTR lpCmdLine, int nCmdShow) {

へ変更します。(注)

注:BCC102のbcc32c.exeでコンパイルする際には"-tU"オプション(ウィンドウプログラム("-tWU")、コンソールプログラム("-tCU"))を使う必要があります。

 

④マクロテーブルの変更

BCCSkeltonでは、外部コールバック関数をマクロで記述してCWNDからCMyWndクラスを派生してアプリケーションに使用していますが、ECCSkeltonではコールバック関数をクラス関数に替えた為、定型ウィンドウメッセージについてはマクロテーブルが不要になりましたが、

(i)  WM_COMMANDメッセージの場合は、OnCommand(wParam, lParam);という関数を呼ぶよう宣言し、これを"CMDTABLE"というマクロにしました。また、この関数の定義もマクロ化し、

//CMyWndクラスのOnCommand()関数定義
BEGIN_CMDTABLE(派生クラス名)
    ON(メニュー・コントロールID, On関数名())
    ON(IDM_VERSION, OnVersion())  //例示です
END_CMDTABLE

の様に記述することにしました。

また、サンプルのFileHandlerの様に既定外のウィンドウメッセージ(WM_SHELLCHANGE)がある場合、既定のメッセージ以外が処理されるOnOthers関数をオーバーライド処理し、そのメッセージの処理関数(OnShellChange(WPARAM, LPARAM))を呼ぶようにします。具体的には、

    //特殊なメッセージ処理
    bool OnOthers(UINT, WPARAM, LPARAM);
    bool OnShellChange(WPARAM, LPARAM);
のようなメンバー関数の宣言を行い、

    //特殊なメッセージ処理(既定のメッセージ以外のメッセージの場合の処理)
    bool CMyWnd::
OnOthers(UINT Message, WPARAM wParam, LPARAM lParam) {    //その他のメッセージ

        if(Message == WM_SHELLCHANGE)
            return OnShellChange(wParam, lParam);
    }

のようにOnOthers関数を定義します。

 

以上の対応でまず100%BCCSkeltonで開発できるプログラムはECCSkeltonで開発できるので、BCCFormやSkeltonWizardも依然利用可能です。なお、プログラムのコンパイルには"-tU"オプションが使える、BCCSkeltonで記述したBatchGood.exeを使用することを推奨します。

 

ps. なお、サンプルはWCEditorの他、駆け込みで今日完成したCSHELLとCSPLITを使ったFileHandlerをアップしていますが、肝心のCREDITクラスのサンプルはまだなので、これからなんかこれを使ったアプリを作って不具合チェックを行います。

 

20年前のBorland C++ Compiler(BCC551)の為のBCCSkeltonの正当な後継として、ワイド文字(UTF-16であるWCHAR(wchar_t))をデフォルトとする現代のWindowsや、C++11に即したEmbarcadero C++Compiler(BCC102)の為の「SKDプログラミング省力化ライブラリー」としてECCSkeltonに手を付けましたが、現在BCCSkeltonの主だった31のクラスは移植済で、後残されたクラスは、

 

CFILE(これはバイナリー用なのか、char文字列用なのかはっきりしない不良クラスで、使用を見直すつもり。)

CMDI(Multiple Documents Interfaceは、私見では、使いがってから終わっているかも、と感じられます。)

と、後はCOMを利用する為に書いた

CVOICE(TTS-Text_To_Speech用です)

CSHELL(Shellを利用する為です)

CDSHOW(DirectShow用です)

だけになりましたので、先ず

 

「従前のBCCSkeltonWplusは廃止し、ECCSkeltonへ移行します」(注)

 

と宣言をして、BCCFormandBCCSkeltonパッケージにECCSkeltonとサンプルコードを入れて配布します。(BCCSkeltonWplusフォールダーと収容ファイルは廃止です。)

注:【無駄話】私説「Unicodeの世界と歴史」~技術、文化~以降、WCEditor関連のブログが関係します。

  記事検索でユニコード、Unicode、ワイド文字などで検索してください。

 

現在、シンタックスチェックは終了しましたが、実際にアプリに組み込んだ動作試験はこれからですので、今度クラスの修正の可能性は大いにあるという点、ご了解願います。

 

ECCSkeltonのCSTRも無事完成し(従前のBCCSkeltonWplusのものにSJIS対応させました)、WCEditorもしっかり動くようになったので、しつこくCANVASとCARGをワイド文字対応させ、ついでにサンプルもBCCSkeltonから移植しようと思ったら、CTBARとCBMPが必要、ということでそちらも手を付けました。

 

そこで、不図気が付きました。

「コモンコントロールって、どうやってワイド文字対応しているのかな?」

 

色々とググってみましたが、その話題の記事は見当たらず、「commctrlW.h」「wcommctrl.h」なども無いようです。では、ツールバーをサンプルに自分で調べてみようと、commctrl.hを覗くと、ツールバー部分では、

//====== TOOLBAR CONTROL ======

#ifdef  UNICODE
#define TOOLBARCLASSNAME        TOOLBARCLASSNAMEW
#else
#define TOOLBARCLASSNAME        TOOLBARCLASSNAMEA
#endif


#ifdef UNICODE
#define TBSAVEPARAMS            TBSAVEPARAMSW
#define LPTBSAVEPARAMS          LPTBSAVEPARAMSW
#else
#define TBSAVEPARAMS            TBSAVEPARAMSA
#define LPTBSAVEPARAMS          LPTBSAVEPARAMSA
#endif

ifdef UNICODE
#define TB_GETBUTTONTEXT        TB_GETBUTTONTEXTW
#define TB_SAVERESTORE          TB_SAVERESTOREW
#define TB_ADDSTRING            TB_ADDSTRINGW
#else
#define TB_GETBUTTONTEXT        TB_GETBUTTONTEXTA
#define TB_SAVERESTORE          TB_SAVERESTOREA
#define TB_ADDSTRING            TB_ADDSTRINGA
#endif

ifdef UNICODE


#define TB_MAPACCELERATOR       TB_MAPACCELERATORW
#else
#define TB_MAPACCELERATOR       TB_MAPACCELERATORA
#endif

.(まだまだ続きます)

 

のように、このヘッダーファイルの中で識別しています。と、いうことでユーザーは(特に文字列を扱わないコントロールでは)別途MSから指示が無い限り、ワイド文字対応関数を調べる必要は無いようです。(しかし、"#define UNICODE"をお忘れなく。)

 

ということで、CTBARはそのまま修正しないでOKでした。(不具合が出たらまたレポートしますね。)

 

前回

== 引用 ==

以上ですが、

(ア)ファイルデータはサクラエディターで作成、DumpでNULL終端付きで保存されていることを確認済。

(イ)Buffは毎回動的にメモリーを確保しているし、ファイルデータ(文字列)の最後にはNULL終端が付いている。

(ウ)にもかかわらず、↑の現象が発生することを考えると、

   (i)  ファイルデータがBuffに読み込まれた段階でごみ問題が発生しており、

   (ii) Buffが同じアドレスで生成され、ReadFile関数でファイルデータのNULL終端が読み込まれない。

   と考えるしかないのではないでしょうか?

分からない...

== 引用 ==

から、悩みに悩みましたが、結局実験と観察を繰り返してやっとこさ解決しました。

 

まず、

1.「(ア)ファイルデータはサクラエディターで作成、DumpでNULL終端付きで保存されていることを確認済。」

これは間違いでした。Dumpで文字列配列[ファイルサイズ]がNULLであったので誤解しました。本来は文字列配列[ファイルサイズ - 1]が最終文字であり、それは(文字列の最後を確認する為につけた)’X’でした。要すれば「ファイルからNULL終端無しの文字列をバッファに読み込んだ」為に文字列の後にゴミが付くことになったわけです。(注)

注:GetFileSize関数の「ファイルサイズ」が具体的に何を意味するのか、はMicrosoft Docにも書かれていませんが、「書き込まれたバイト数」だと思っていました。(詳しく調べようとして、Windows NTFSや、そのEOF関連を調べると頭が痛くなります。)文字列データの書き込みでは常にNULL終端を入れてセーブするものと考え、ファイルサイズもNULL終端のバイト数まで含むものと理解していましたが、そういえば、それを確認したことはありませんでしたね。

 

次に、

2.「(イ)Buffは毎回動的にメモリーを確保しているし、ファイルデータ(文字列)の最後にはNULL終端が付いている。」

これはその通りであり、「LPBYTE Buff = new BYTE[dwFileSize]();」の"()"を付けることでゼロ初期化されるのですが、これが正しくなかった(NULL終端が無い)ので、NULL終端があるという前提のMultiByteToWideChar関数の処理が、確保したBuff配列以降のメモリー迄読んでしまい、ゴミも変換されてしまったということです。これはUTF-8のみならず、WCHARでもNULL終端を付ける為に「LPBYTE Buff = new BYTE[dwFileSize + 2]();」として読込バッファを確保すれば解決することになる筈です。

 

余談ですが、

3.「(ii) Buffが同じアドレスで生成され、ReadFile関数でファイルデータのNULL終端が読み込まれない。」

如何に「動的にメモリーを確保」しても、それは「前に開放したメモリー領域ではない」ということと等価ではないので、「同じ場所にプログラムとOSがメモリーを割り当てる」ことは別に不思議ではなく()、「長 ー い 文 章 の サ ン プ ル」を読み込んだ後、(NULL終端のない)「短い文章のサンプル」を読み込むと、解放した「長 ー い 文 章 の サ ン プ ル」の場所に「短い文章のサンプル」の長さだけゼロ初期化された結果、バッファには「          の サ ン プ ル」というデータが並び、この下線部にファイルデータが上書きされた、というのが今回の現象の機序でした。

注:実際、実験を行い、バッファーとそれ以降のメモリーを確認して、解放されたメモリー領域にまたメモリーが確保されることも確認しています。

 

ということで、上記の知見に基づくコード修正を行い、ついでなのでShift-JIS対応も行って最終コードを以下に紹介します。(実験用のトラップのコード込みです。)

<最終的なコード>

    //ファイルを読み込む
    BOOL bSuccess = FALSE;
    HANDLE hFile = CreateFileW(FileName, GENERIC_READ, FILE_SHARE_READ, NULL,
                        OPEN_EXISTING, NULL, NULL);        //ファイルを読込みでオープン
    if(hFile != INVALID_HANDLE_VALUE) {                    //オープンできたか
        DWORD dwFileSize = GetFileSize(hFile, NULL);
        if(dwFileSize != 0xFFFFFFFF) {                    //-1は(0xFFFFFFFF)エラー
            LPBYTE Buff = new BYTE[dwFileSize + 2]();    //読み込みバッファ(NULL終端分追加)用ポインター
            //"new 配列[]()"でゼロ初期化し、WCHARファイルにも対応できるよう最後の2バイトをNULLにしている

MessageBoxA(0, (char*)Buff, "Buffの初期状態", MB_OK);

            DWORD dwRead;
            if(ReadFile(hFile, Buff, dwFileSize, &dwRead, NULL)) {

char str[256];
wsprintfA(str, "File size = %d and bytes read = %d", dwFileSize, dwRead);
MessageBoxA(0, str, "File size in byte", MB_OK);
MessageBoxA(0, (char*)Buff, "Buff in Ascii", MB_OK);

                bSuccess = TRUE;                         //読み込み成功
                //BOM付UTF-16、UTF-32の場合
                if(Buff[0] == 0xFF && Buff[1] == 0xFE) {
                    if(Buff[2] | Buff[3]) {                //いずれもNULLではない→UTF-16のBOM
                        delete [] m_str;                //元の文字列を廃棄
                        //BOM以降のデータをm_strに収納する
                        m_str = new WCHAR[dwFileSize / sizeof(WCHAR)];
                        //正確には"new WCHAR[(dwFileSize + 2 <NULL> - 2 <BOM>) / sizeof(WCHAR)]
                        CopyMemory(m_str, Buff + 2, dwFileSize - 2);    //BOMを除外
                    }
                    else {                                //Buff[2]とBuff[3])がいずれもNULL(UTF-32またはUTF-16でNULLのみ)
                        bSuccess = FALSE;                //BOM付UTF-32の場合、読み込み失敗扱い
                    }
                }
                //BOM付UTF-8の場合
                else if(Buff[0] == 0xEF && Buff[1] == 0xBB && Buff[2] == 0xBF) {
                    dwRead = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)(Buff + 3), -1, NULL, NULL);
                    if(dwRead) {    //UTF-8文字列の場合
                        //m_str用メモリーの初期化
                        delete [] m_str;
                        m_str = new WCHAR[dwRead];
                        //UTF8をWCHARへ変換
                        MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)(Buff + 3), -1, m_str, dwRead);
                    }
                    else {            //BOM付UTF-8で変換不能の場合
                        bSuccess = FALSE;    //何もせず読み込み失敗扱い
                    }
                }
                //UTF-16、32、8、何れのBOMもない場合
                else {
                    //先ずBOM無しUTF-8か否かをチェック
                    if(dwRead = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, NULL, NULL)) {
                        //m_str用メモリーの初期化
                        delete [] m_str;
                        m_str = new WCHAR[dwRead];
                        //UTF8をWCHARへ変換
                        MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, m_str, dwRead);
                    }
                    //次にSift JIS(コードページCP_ACP)か否かをチェック
                    else if(dwRead = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, NULL, NULL)) {
                        //m_str用メモリーの初期化
                        delete [] m_str;
                        m_str = new WCHAR[dwRead];
                        //SJISをWCHARへ変換
                        MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, m_str, dwRead);
                    }
                    //BOMが無く、UTF-8またはSJISではない場合、読み込み失敗扱い←「丸呑み」はやめました
                    else {
                        bSuccess = FALSE;
                    }
                }
            }
            delete [] Buff;                            //読み込みバッファ解放
        }
        CloseHandle(hFile);                            //ファイルのクローズ
    }
    return bSuccess;
 

なお、ファイルの書き込みは特に問題は生じず(注)、

    bool ToFile(WCHAR*, WCHAR*, HWND);  //ダイアログを使ったUTF-16ファイルへの書き込み
    bool ToFile(WCHAR*);                            //UTF-16ファイルへの書き込み
    bool ToUTF8File(WCHAR*, bool);            //UTF-8ファイルへの書き込み(boolはBOM有無のフラグ)
    bool ToSJISFile(WCHAR*);                     //SJISファイルへの書き込み

という4種類の関数を用意しました。

 

注:実はWCEditorの本体のコーディングで誤解から、EDITコントロールの文字数が一つ少なくなるトラブルがあり、これも(昔は分かっていたのですが、すっかり忘れてしまい)誤解から生じていたことが分かりました。皆様もご注意ください。

 

WM_GETTEXTLENGTH→戻り値はテキストの文字数です。この文字数には終端ヌル文字は含まれません
WM_GETTEXT→wParamには取得するテキストの文字数を入れますが文字数には終端ヌル文字も含まれます。(WM_GETTEXTLENGTHで取得した値に+1しなければならない)また、戻り値はコピーされたテキストの文字数が返ります。この文字数には終端ヌル文字は含まれません。

 

なお、サクラエディタ―でつくったサンプルとWCEditorで保存したファイルを比較するとUTF-16で2バイト、UTF-8やSJISで1バイトファイルサイズが異なります。これはWCEditorがNULL終端迄保存しているため(↓)ですが、これを止めるか否か考え中です。(今回CSTRはサクラエディターと同じようにNULL終端無しにセーブされたデータをNULL終端を付けて読み込む形にしたので、NULL終端無しに保存する方が合理的です。しかし、今回のような問題からもNULL終端を付けたデータの方がより安全であるということも言えます。NULL終端を付けたファイルを読み込んで、更に末尾にNULL終端が追加されても、再度書き込む際にはNULL終端は一つだけつけるので、ファイルサイズが膨張してゆくことはありません。)

 

<UTF-16>

        //バッファから文字列長取得し、NULL終端も加算
        DWORD dwTextLength = (lstrlenW(m_str) + 1) * sizeof(WCHAR);
<UTF-8>

        //WCHARをUTF8へ変換した際の文字列長(バイト-NULL終端を含む)を取得
        DWORD dwBuffSize = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, m_str, -1, NULL, NULL, NULL, NULL);
<SJIS>

        //WCHARをSJISへ変換した際の文字列長(バイト-NULL終端を含む)を取得
        DWORD dwBuffSize = WideCharToMultiByte(CP_ACP, WC_ERR_INVALID_CHARS, m_str, -1, NULL, NULL, NULL, NULL);

 

いずれにしても、これでECCSkeltonのCSTRクラスは一応の完結を迎えたと考えて良いでしょう。

 

しっかし、今回のバグ取りは疲れました!

情けないことに、まだ謎は解けません。

UTF-16問題の後、UTF-8(BOM付、無共に)を調べていますが、(1)のコードで、(2)のような現象が生じています。

 

(1)問題のコード(抜粋)

    //ファイルを読み込む
    BOOL bSuccess = FALSE;
    HANDLE hFile = CreateFileW(FileName, GENERIC_READ, FILE_SHARE_READ, NULL,
                        OPEN_EXISTING, NULL, NULL);    //ファイルを読込みでオープン
    if(hFile != INVALID_HANDLE_VALUE) {                //オープンできたか
        DWORD dwFileSize = GetFileSize(hFile, NULL);
        if(dwFileSize != 0xFFFFFFFF) {                //-1は(0xFFFFFFFF)エラー
            LPBYTE Buff = new BYTE[dwFileSize]();    //読み込みバッファ用ポインター
            DWORD dwRead;
            if(ReadFile(hFile, Buff, dwFileSize, &dwRead, NULL)) {

MessageBoxA(0, (char*)Buff, "Buff in Ascii", MB_OK);    //トラップ

                bSuccess = TRUE;                     //読み込み成功
                if(Buff[0] == 0xFF && Buff[1] == 0xFE) {//UTF-16、UTF-32のBOMチェック
                    if(Buff[2] | Buff[3]) {            //いずれもNULLではない→UTF-16のBOM
                        delete [] m_str;            //元の文字列を廃棄して
                        //BOM以降の読み込んだデータを入れられるようにする
                        m_str = new WCHAR[(dwFileSize - 2) / sizeof(WCHAR) + 1];    //一文字多くする
                        CopyMemory(m_str, Buff + 2, dwFileSize - 2);
                        m_str[(dwFileSize - 2) / sizeof(WCHAR)] = L'\0';            //追加一文字をヌルにする
                    }
                    else {    //Buff[2]とBuff[3])がいずれもNULL→UTF-32のBOM(またはUTF-16でNULLのみのファイルの場合)
                        bSuccess = FALSE;            //UTF-32BOMの場合、何もせず読み込み失敗扱い
                    }
                }
                else if(Buff[0] == 0xEF && Buff[1] == 0xBB && Buff[2] == 0xBF) {    //UTF-8のBOMチェック
                    dwRead = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)(Buff + 3), -1, NULL, NULL);
                    if(dwRead) {                    //UTF-8文字列の場合
                        //m_str用メモリーの初期化
                        delete [] m_str;
                        m_str = new WCHAR[dwRead];
                        //UTF8をWCHARへ変換
                        MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)(Buff + 3), -1, m_str, dwRead);
                    }
                    else {                    //BOM付きUTF-8で変換不能な場合
                        bSuccess = FALSE;    //何もせず読み込み失敗扱い
                    }
                }
                else {                        //UTF-16、32、8のBOMがない場合
                    //先ずBOM無しUTF-8か否かをチェック
                    dwRead = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, NULL, NULL);
                    if(dwRead) {            //BOM無しUTF-8の場合
                        //m_str用メモリーの初期化
                        delete [] m_str;
                        m_str = new WCHAR[dwRead];
                        //UTF8をWCHARへ変換
                        MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, m_str, dwRead);
                    }
                    else {                    //BOMが無く、UTF-8変換できない場合「丸のみ」

MessageBoxW(0, L"丸呑みします。", L"BOM無し", MB_OK);    //トラップ

                        m_str = new WCHAR[dwFileSize / sizeof(WCHAR) + 1];
                        CopyMemory(m_str, Buff, dwFileSize);
                        //念のため、バッファの終端にNULLを置く
                        m_str[dwFileSize / sizeof(WCHAR)] = L'\0';
                    }
                }
            }
            delete [] Buff;                            //読み込みバッファ解放
        }
        CloseHandle(hFile);                            //ファイルのクローズ
    }

注:いずれは「丸のみ」をSJISくらいは入れてみるつもりですが、今はそれどころではないので。

 

(2)UTF-8のサンプル(BOM付、無し共に短、長の2種類)で試験した際の現象

(サンプルは最後に"(改行)X"(0DH、0AH、58H、00H)が入り、以下①~のシーケンスを連続して試験実施)

①UTF-8BOM付のサンプル(短、長の2種類)の読み込み

→想定通りの正常動作

②UTF-8BOM無のサンプル(短)の読み込み

→どういう訳か、MultiByteToWideCharで変換できず「丸呑みします」と表示され、文字化けする。(上記①の代わりにBOM無サンプルを読み込むと正常に読み込め、表示される。)

③UTF-8BOM無のサンプル(長)の読み込み

→想定通りの正常動作

④再度UTF-8BOM無のサンプル(短)の読み込み

→今度はサンプルが変換されて、表示されるが、次のようにサンプル(長)のデータがごみとなって残る。

BCCForm and BCCSkeltonブログを読んでいただいてありがとうございます。
これからもよろしくお願いいたします。
X
have become 20 years of age and I will yet keep doing my best.
X(赤字部分がごみ)

⑤再度UTF-8BOM付のサンプル(短)の読み込み

→同様に次のようにサンプル(長)のデータがごみとなって残るが、3文字(Xの後のスペース)減少しているのは、UTF-8のBOMが3文字だからかと考えられる。

BCCForm and BCCSkeltonブログを読んでいただいてありがとうございます。

これからもよろしくお願いいたします。
X
ve become 20 years of age and I will yet keep doing my best.
X

⑥再度UTF-8BOM無のサンプル(短)の読み込み

→"(改行)X"がごみとなって残るのは、UTF-8のBOM3文字(OD、OAとX)の差かと考えられる。

BCCForm and BCCSkeltonブログを読んでいただいてありがとうございます。
これからもよろしくお願いいたします。
X

X

 

以上ですが、

(ア)ファイルデータはサクラエディターで作成、DumpでNULL終端付きで保存されていることを確認済。

(イ)Buffは毎回動的にメモリーを確保しているし、ファイルデータ(文字列)の最後にはNULL終端が付いている。

(ウ)にもかかわらず、↑の現象が発生することを考えると、

   (i)  ファイルデータがBuffに読み込まれた段階でごみ問題が発生しており、

   (ii) Buffが同じアドレスで生成され、ReadFile関数でファイルデータのNULL終端が読み込まれない。

   と考えるしかないのではないでしょうか?

 

分からない...

UTF-16の読み込みは安定したみたいですので、UTF-8をリビューします。

 

現在UTF-8はBOM(0xEF、0xBB、0xBF)付とBOM無しの何れも読めるようにする仕様で、変換は(コンストラクターでSetLocaleを実行し)MultiByteToWideChar関数(注)の二度使いで1パスでワイド文字列長を得、2パスで実際に変換するようにしています。

注:この関数の第4引数であるint cbMultiByteを-1にして、NULL終端付文字列として使っています。従って、1パスで返される文字列長は最終のNULLが含まれることを実際に実験して確認しています。

 

現在WCEditorで読み込み実験(注)をすると、

(1)UTF-16ファイルは正常に読み込みます。

(2)BOM付UTF-8も正常に読み込みます。

(3)その後、BOM無UTF-8を読み込むと文字化けします。再度BOM無UTF-8ファイルを読むと正常に読み込めます。(最初の一回だけ文字化けするようです。

(4)UTF-8で、長いファイルを読んだ後、短いファイルを再度読み込むと、そのファイルのお尻に前のファイルのゴミがそのまま残ります。(読み込みバッファはBYTE配列を動的に確保しており、最後に開放しているのですが...また、新しく読み込んだファイルのNULL終端が無いことも謎です。

という症状が残ります。

注:デバグ検証のために、サクラエディタ―でBOM付UTF-16、BOM付UTF-8、BOM無UTF-8のサンプルファイルを二つづつ作り、これを読み込んだり、読み込んだ文字列をかき出して実験しています。

 

まぁ、気長に仮定と実験で対処してゆくことにします。

 

皆さま、お暑うございます。

暑すぎてプログラミングから遠ざかっていましたが、前に触れましたECCSkeltonのCSTRクラスの不具合に関し、

【ECCSkelton】またまたUnicode-う~ん、分からん

ボチボチとバグ取りプロセスなど、始めてゆこうと思います。先ずはUTF-16の読み込み不具合から。

 

【オリジナルのコード】

    //ファイルを読み込む
    BOOL bSuccess = FALSE;
    HANDLE hFile;
    hFile = CreateFileW(FileName, GENERIC_READ, FILE_SHARE_READ, NULL,
                        OPEN_EXISTING, NULL, NULL);    //ファイルを読込みでオープン
    if(hFile != INVALID_HANDLE_VALUE) {                //オープンできたか
        DWORD dwFileSize = GetFileSize(hFile, NULL);
        if(dwFileSize != 0xFFFFFFFF) {                //-1は(0xFFFFFFFF)エラー
            LPBYTE Buff;                            //読み込みバッファ用ポインター
            Buff = new BYTE[dwFileSize];
            DWORD dwRead;
            if(ReadFile(hFile, Buff, dwFileSize, &dwRead, NULL)) {
                bSuccess = TRUE; //読み込み成功
            }
            if(Buff[0] == 0xFF && Buff[1] == 0xFE) {//UTF-16、UTF-32のBOMチェック
                if(Buff[2] | Buff[3]) {                //いずれもNULLではない→UTF-16のBOM
                    delete [] m_str;                //元の文字列を廃棄して
                    //BOM以降の読み込んだデータを入れられるようにする
                    m_str = new WCHAR[dwFileSize / sizeof(WCHAR)];
                    CopyMemory(m_str, Buff + 2, dwFileSize - 2);
                    //念のため、バッファの終端にNULLを置く
                    m_str[(dwFileSize - 2) / sizeof(WCHAR) - 1] = L'\0';
                }
                else {    //Buff[2]とBuff[3])がいずれもNULL→UTF-32のBOM(またはUTF-16でNULLのみのファイルの場合)
                    bSuccess = FALSE;    //UTF-32BOMの場合、何もせず読み込み失敗扱い
                }
            }


先ずファイルの内容をBYTE配列(符号なし8ビット変数char配列)のBuffに読み出し、ファイルサイズ(dwFileSize)を求めてから、先頭のBOMを調べてUTF-16ならBOMの後のデータを(2バイトのBOM込みファイルサイズ相当の)WCHAR配列m_strにコピーしますが、最後にWCHAのNULL終端を入れておいた方が安全であろうということで入れたのは良かったのですが、これが文字列データの最後に引っかかっていたというのが原因でした。

 

別の言い方をすると、ファイルは前に書いた通り、

2.ダンプリスト

0000 FF FE 42 00 43 00 43 00 :46 00 6F 00 72 00 6D 00 :..B.C.C.F.o.r.m.
0010 20 00 61 00 6E 00 64 00 :20 00 42 00 43 00 43 00 :..a.n.d...B.C.C.
0020 53 00 6B 00 65 00 6C 00 :74 00 6F 00 6E 00 D6 30 :S.k.e.l.t.o.n..0
0030 ED 30 B0 30 92 30 AD 8A :93 30 67 30 44 30 5F 30 :.0.0.0...0g0D0.0
0040 60 30 44 30 66 30 42 30 :8A 30 4C 30 68 30 46 30 :.0D0f0B0.0L0h0F0
0050 54 30 56 30 44 30 7E 30 :59 30 02 30 0D 00 0A 00 :T0V0D0.0Y0.0....
0060 53 30 8C 30 4B 30 89 30 :82 30 88 30 8D 30 57 30 :S0.0K0.0.0.0.0W0
0070 4F 30 4A 30 58 98 44 30 :44 30 5F 30 57 30 7E 30 :O0J0X.D0D0.0W0.0
0080 59 30 02 30 0D 00 0A 00 :00 00 00 00 00 00 00 00 :Y0.0............

<説明>

ダンプの8バイトコードはIntelチップがリトルエンディアンなのでひっくり返って格納されます。(例:FEFF→FF FE)

FEFF → UTF-16 BOM

U+3059 → 「す」

U+3002 → 「。」

U+000D → CR(注)

U+000A → LF(注)

注:改行コードはOSによって異なります。WindowsはCR+LF、MacはCRのみ、UnixはLFのみです。

なのですが、最後の「00 0A」にNULL終端がかぶされた為に改行コードが消され、「00 0D」がゴミ(末尾の■)として残った、という顛末でした。

 

現在のコードは分かりやすくするために青字部分の内容は同じでも、「BOMの後のデータに加え一文字分余計に確保する」ことが分かりやすくし、NULL終端を追加部分に置くことが分かりやすくなるように(緑字部分)、次のようにしています。

【修正コード】

    //ファイルを読み込む
    BOOL bSuccess = FALSE;
    HANDLE hFile;
    hFile = CreateFileW(FileName, GENERIC_READ, FILE_SHARE_READ, NULL,
                        OPEN_EXISTING, NULL, NULL);    //ファイルを読込みでオープン
    if(hFile != INVALID_HANDLE_VALUE) {                //オープンできたか
        DWORD dwFileSize = GetFileSize(hFile, NULL);
        if(dwFileSize != 0xFFFFFFFF) {                //-1は(0xFFFFFFFF)エラー
            LPBYTE Buff = new BYTE[dwFileSize];        //読み込みバッファ用ポインター
            DWORD dwRead;
            if(ReadFile(hFile, Buff, dwFileSize, &dwRead, NULL)) {
                bSuccess = TRUE;                     //読み込み成功
                if(Buff[0] == 0xFF && Buff[1] == 0xFE) {//UTF-16、UTF-32のBOMチェック
                    if(Buff[2] | Buff[3]) {            //いずれもNULLではない→UTF-16のBOM
                        delete [] m_str;            //元の文字列を廃棄して
                        //BOM以降の読み込んだデータを入れられるようにする
                        m_str = new WCHAR[(dwFileSize - 2) / sizeof(WCHAR) + 1];    //一文字多くする
                        CopyMemory(m_str, Buff + 2, dwFileSize - 2);
                        m_str[(dwFileSize - 2) / sizeof(WCHAR)] = L'\0';            //追加一文字をヌルにする
                    }
                    else {    //Buff[2]とBuff[3])がいずれもNULL→UTF-32のBOM(またはUTF-16でNULLのみのファイルの場合)
                        bSuccess = FALSE;            //UTF-32BOMの場合、何もせず読み込み失敗扱い
                    }

 

次はUTF-8の読み込みをリビューしてゆきます。

 

暑くてプログラミングに意欲がわかないのと、前回のスマホ不具合の話で懐かしい「リスク管理」の語を使ったことから、プログラミングには関係ないですが、前職ベースの話を一寸させてください。

 

今朝の新聞で警察庁長官談話、先般の銃撃事件の警護についてのリビュー(従ってそれに伴う責任問題)が触れられていました。

確かに今回の現場の状況(駅前のロータリーの、中央のガードレールで囲まれただけの「中州」で全員が駅方向に向いていた)は、「銃器等による襲撃()」に関して言えば後方の公道は勿論、周囲に対して無防備であったと言えます。

注:今回は後方の公道にあるバス停に潜伏していた加害者が中州に向かって射撃しましたが、後方を通過する車両からの襲撃(爆発、延焼物による危害を含む)や周囲の高層建物からの狙撃、遠隔地からの砲撃等にも無防備です。元首相の周囲を奈良県警と警察庁SPが警護する~きわめて「一般的な図」ですが~警護体制は「危害の想定が、加害者が刀剣等による切り付け等の接近攻撃を想定したもの=銃器規制の強い日本の環境」に基づく「想定」です。実際、要人が銃撃された事件はありましたが、平成2、19年の長崎市長銃撃事件や国松長官銃撃事件の他は甚大な被害が出なかったことがその根拠になるのでしょう。

 

それでは一般論として、銃撃危害に対してどのような対処が必要なのでしょうか?

基本的に銃撃危害の不確実性がある場合、被危害者(危害に晒される者)は外出せず、防弾状況の良い場所にとどまることが「リスクの回避(risk abandonment)」、外出する際に影武者を使ったり、人の盾(今回もこの論議が出ていますが)で防御することが「リスクの移転(risk transfer)」と(その他の防御方策による)「リスクの軽減(risk reduction)」、今回の様に危害が発生するか否かという不確実性を引き受ける「リスクの保有(risk retention)」というリスク対処があります。

 

これを具体的に言うと、

(1)一番安全なのはリスクの回避ですが、これを行うと活動ができずに往々にして被危害者の方が嫌う傾向にあります。

(2)どうしても応援演説を行うならば、(今回の様に)「後ろを取られないように」演説場所や設定による銃撃危害への対処(物的対応)と事前の危害情報の取得、専門要員による周辺監視、防御要員による対処体制(人的対応)を敷いての実施

(3)(リスクー危害による損失の不確実性-はゼロにはならないので、一定のリスクを覚悟で)実施

というようなものですが、(2)と(3)は(更に(1)も)実は本質的な差がないことがお判りでしょう。

 

これは、

①銃撃危害があることが認知された場合、

②それによる損失を算定、重大性を評価し、

③それに対する対処の方策を検討、列挙して、

④危害の不確実性(蓋然性) vs. 対処の十分性による「リスクが受忍できるか否かの判断」を行い、(上記(2)参照)

⑤(④でも取り除けない)残留リスクが受忍できないものである可能性(不確実性)の「リスク」を引き受けるか否か(上記(3)参照)

によって異なるだけです。(ご参考

 

より具体的に言えば、今回の事件の際に、

(a) 銃撃リスクがあるか、どの程度あるか?(リスクの特定、算定、評価)

(b) 銃撃に対する物的、人的対応は万全か?(方策の想定、選定)

(c) 想定が難しい危害のリスクが残るが、甘んじて実施するか?(残留リスクへの対処)

について、どのように判断、行動したか、が合理的で適切であったかがリスク管理上の要点であり、それを事後検討すること(PDCAサイクルの"Check - Action"の部分)もリスク管理上重要となります。

 

しかし、この一般的リスク管理プロセスの結論(合理的で適切であったか、否か)は銃撃危害の蓋然性の程度によって異なります。例えば、

・「元総理の議員を含む全議員の警護」を、今後一律に高度警護レベルへの引き上げることが合理的でしょうか?

・「社会的に認知されていない泡まつ候補」に対しても現職議員と同じ警護レベルにすることが合理的でしょうか?

・「元総理は、他の議員や一般人候補よりも銃撃危害リスクが高い」と考えるのが合理的でしょうか?その根拠は何でしょうか?

 

その答えの基礎の一つになるのが「情報処理による意思決定」、”Intelligence"(そのプロセスが"Intelligence Cycle")でしょう。

国家安全保障であればCommint、Humintなどの諜報的レベル、刑事捜査機関であれば公安捜査により、事前のリスク特定や評価という情報処理による意思決定が可能でしょうが、今回の様に「職場を辞め、人付き合いの乏しい個人が自宅で単独の計画や行動をとっていた」場合、事前の情報取得は非常に困難であり、「個人による銃撃の一般的な想定()」がなされたか、どのような想定がなされたか、で対処せざるを得なかったでしょう。

注:不特定の個人が被危害者の生命の危害を与える、①動機、②方法、③機会、④実行能力が可能か否か。今回の場合、①殺人を企図するまでの怨恨、②自作銃器、爆薬、銃弾の調達・作成、③時間的、地理的な機会、④接近・銃撃能力が認められましたが、①の想定が可能であったかは不問としても、②④は銃器を扱う訓練を受けた警察官や自衛官等のキャリアやネットを通じた自作銃器の製造はリスク想定が可能であったと考えられれば、それに応じた③の検討が合理的、適切であったかは検討可能でしょう。

 

となると、「個人による銃撃の一般的な想定」に対して、「毀誉褒貶の激しかった元首相」という被危害者の評価からどの程度のリスク対処(警戒・警護)レベルとしたか、その判断の内容、合理性、適切性が今後のリビューの対象になるのではないでしょうか?

 

と、ここまで書いて矢張り「リスクの蓋然性の程度」の判断は"Hindsight"(後知恵)に流れる傾向があることは、否定しません。事象が未発生の時点での判断の適否を、実際に事象が発生した後で検証すれば、矢張り(望まれる結論を志向して)何れにも流れ得る、ということです。

 

警察庁長官に残ってほしい勢力が強いなら「今回の銃撃は想定外であった」、椅子を狙う勢力が強いなら「銃撃想定は可能であり、それに基づく警護が十分ではなかった」という結論は何れも可能でしょう。

 

このような"Hindsight"(後知恵)問題の好例は「東電原発事故における2009年『御前会議』における被告役員判断の合理性()」でしょう。"Presumed innocence"ベースの刑事責任はなかったとしても、"preponderance of evidence"ベースの民事訴訟でどうなるのか、役員責任訴訟の判決が7月13日に予定(追記)されているようなので、個人的には興味があります。

注:2008年に15.7mの津波があり得るという報告が出されたが、経営判断は従前の5.7mの想定を継続した。

追記:本件は東電の株主が会社の利害を代表して役員に22兆円の損害賠償責任を求める株主代表訴訟であり、本日の判決では13兆円の損害について賠償責任を認めました。(原告株主の一審勝訴)

ECCSkeltonのCSTR問題は進んでいません。何しろ暑くて、暑くてプログラミングの意欲が出ない。少し涼しくなってから考えます。

 

それより、巷は色々と騒がしかったですね。auの通信障害や参院選、安部元首相銃撃など、夏なのに世の流れは活発です。そしてその最中(さなか)、私のスマホが突然ご臨終となりました。経緯は次のようなものです。

 

早朝、起床して日課のウオーキングに出ます。無線イヤーフォーンで私の3GB以上のiTuneの「懐かしのメロディ」(注)を聞くのが本当の目的でもあります。

この日は帰宅して電池残量が低いので充電し、朝食のサラダを(カミさんの分も)作り、入浴(夏場はシャワー)して、その後トマトジュースを飲みながらじっくりと新聞を読む、というルーティーンを続けます。

注:といっても演歌ではありません。1960-1980年代の基本Jazz(Smooth系が好きですね)、Rock、Pop、歌謡曲なんでもござれです。

 

で、その後気づいたんです。iPhoneの電池残量が増えていないことに。更にフェースIDも効かなくなります。そして対処の仕様もなく、電池切れでご臨終。

 

翌日に〇a〇a〇a電機のApple指定修理デスクへ行き、予約はなかったのですが、当日予約で相談ができました。しかし、(「iPhoneを探せ」を殺すための)Apple IDが分からず、いったん帰宅。そして再度修理依頼を完了しました。

 

二日後、矢張り修理は不可ということで全交換で修理完了し、昨日黒のXRを回収してきたところです。

 

で、ポイントは何かというと、私の「懐かしのメロディ」ライブラリーの為に直前に"Earl Klugh"と"Nancy Wilson"、およびGeorge BensonとKlughの日本でのセッションをiTuneに入れたところだったのでバックアップが取れていたこと。これは本当に運がよかったです。

 

皆さんも予期せぬ通信障害や不具合へのリスク管理、気を付けてくださいね。