2011年9月15日木曜日

64bit環境で動かない.NETアプリ

一部の特殊なアプリを除けば、64bit環境でも従来のアプリはWow64上で普通に動作するものが多いが、たまに.NETアプリは動かないものがある。フックやシェル拡張など他のプロセスに寄生するアプリやドライバならともかく.NETでそんなの書かないのに何故? と不思議に思うだろう。

どういう訳かというと.NETアプリの実行ファイルはコンパイルした段階では純粋なマシン語ではない中間言語で、実行時にCPUのビット数に合ったマシン語に変換される仕組みになっており、同じ実行ファイルでも64bit OS上では64bit, 32bit OS上では32bitで動作する。

ここに一つの罠があって、.NETアプリにはたまにネイティブのDLLを利用しているものがあり、このDLLは始めからマシン語でありコンパイル時に指定したビット数のCPUでしか動作しない。そのため、64bit環境で動作させると.NETアプリのプロセスは64bitで起動して、32bitのDLLを読み込もうとしてエラーとなってしまう。

32bitのネイティブアプリのように始めからWow64上で32bitアプリとして起動させることができれば32bitのDLLでも読み込める。実は.NETアプリのコンパイル時にTarget CPUにx86を選んでいれば、64bit環境でも32bit CPUのマシン語に変換されるため、Wow64が発動してちゃんと動いてくれる。上に書いた罠が発動するのはコンパイル時にTarget CPUにAny CPUを選んでいる場合なのだ。

最近はこのノウハウも広まってきたので、アプリ作者の人もちゃんとコンパイルオプションを選んでくれるし、今動かなくても将来のバージョンアップで動くようになるだろうけど、もう作者の人が更新を止めちゃっている場合はどうしたら良いのか。

実はコンパイルオプションを変えても実行ファイルの大部分である中間言語の部分は全く同じで、ヘッダにあるフラグがちょっと違うだけなので、バイナリエディタで書き換えてやれば良い、というのは大変なのでちゃんとツールがある。

CorFlags.exe "対象の.exe" /32BIT+

問題はこのコマンドは単体では配っていなくて、.NET FrameworkのSDKを入手してインストールしないと使えるようにならないこと。自分はVisual Studio C#のExpress Editionを使っているので始めから入っていたけど、そうじゃない人はちょっと面倒かもしれない。

2011年9月10日土曜日

64bitプロセスから32bitプロセスにDLL Injection (C言語)

ググったら32bit→64bitはNGだけど、64bit→32bitは行けるという情報があったので試したら行けた。DLL Injectionって何?という人は「別のプロセスにコードを割り込ませる3つの方法」にとても詳しく書かれているのでそちらを参考に。(こういうマニアックな記事は好きだ)

面倒なので自作アプリのソースの一部をそのまま貼る。本筋じゃない処理も混ざってるけど自力で読み飛ばし推奨。GOTO_Eは最後のラベルにgotoするマクロ。
bool pAttachRemoteThread(DWORD pid, const char* dllName, bool autoFree, bool waitRemoteSemaphoreRelease){

bool result = false;
HANDLE hThread = NULL;
PWSTR vDllPath = NULL;
HANDLE hProcess = NULL;
HANDLE hSemaphore = NULL;
HANDLE hMutex = NULL;

hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE,
FALSE, pid);

if(hProcess == NULL) GOTO_E;

BOOL isWow64;
IsWow64Process(hProcess, &isWow64);

// DLLのフルパスの編集
char dllPath[MAX_PATH+1];

GetModuleFileName(g_hInst, dllPath, sizeof(dllPath));
PathRemoveFileSpec(dllPath);
PathAppend(dllPath, dllName); //このDLLと同じディレクトリ

if(_stricmp(PathFindExtension(dllPath), ".dll") == 0){ // 末尾の.dllは除去(大小文字無視)
PathRemoveExtension(dllPath);
}

#ifdef _WIN64
if(!isWow64){ // 64bitのDLL名には64を追加
strcat(dllPath, "64");
}
#else
// 32bit版ビルドで64bitのプロセスにアタッチしようとしたらエラー
BOOL iamWow64;
IsWow64Process(GetCurrentProcess(), &iamWow64);
if(iamWow64 && !isWow64) GOTO_E;
#endif

strcat(dllPath, ".dll");

// ロードするDLLのパスをリモートプロセスのメモリに書きこむ。
size_t dllPathLen = strlen(dllPath) + 1;
vDllPath = (PWSTR)VirtualAllocEx(hProcess, NULL, dllPathLen, MEM_COMMIT, PAGE_READWRITE);
if(vDllPath == NULL) GOTO_E;
if(WriteProcessMemory(hProcess, vDllPath, (PVOID)dllPath, dllPathLen, NULL) == 0) GOTO_E;

// Loadlibraryのリモートプロセスでのアドレスを取得
PTHREAD_START_ROUTINE addr = pGetProcAddress(isWow64, "LoadLibraryA");
if (addr == NULL) GOTO_E;

// DLL名 + pidが同じ処理は同時実行を抑止。
char mutexName[FILENAME_MAX + 12 + 1];
sprintf(mutexName, ":%s%d", dllName, pid);
hMutex = CreateMutex(NULL, FALSE, mutexName);
if(WaitForSingleObject(hMutex, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;

// リモートの処理終了を検知するセマフォ(名前:DLL名 + pid)を用意
if(waitRemoteSemaphoreRelease){
char* semaphoreName = pGetSemaphoreName(pid, dllName);
hSemaphore = CreateSemaphore(NULL, 0, 1, semaphoreName);
free(semaphoreName);
if(hSemaphore == NULL) GOTO_E;
}

// LoadLibraryを実行して終了コード判定
hThread= CreateRemoteThread(hProcess, NULL, 0, addr, vDllPath, 0, NULL);
if (hThread == NULL) GOTO_E;

// スレッドの終了待ち
if(WaitForSingleObject(hThread, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;
DWORD exitCode;
GetExitCodeThread(hThread, &exitCode);
if(exitCode == NULL) GOTO_E;

// インジェクションしたDLLがセマフォをリリースするのを待つ。
if(waitRemoteSemaphoreRelease)
if(WaitForSingleObject(hSemaphore, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;

// FreeLibraryを実行
if(autoFree){
addr = pGetProcAddress(isWow64, "FreeLibrary");
if (addr == NULL) GOTO_E;

CloseHandle(hThread);
hThread= CreateRemoteThread(hProcess, NULL, 0, addr, NULL, 0, NULL);
if (hThread == NULL) GOTO_E;

if(WaitForSingleObject(hThread, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;
GetExitCodeThread(hThread, &exitCode);
if(exitCode == NULL) GOTO_E;
}

result = true;

ERROR_HANDLER:
if(vDllPath != NULL) VirtualFreeEx(hProcess, vDllPath, sizeof(dllPath), MEM_RELEASE);
if(hProcess != NULL) CloseHandle(hProcess);
if(hSemaphore != NULL) {
ReleaseSemaphore(hSemaphore, 1, NULL);
CloseHandle(hSemaphore);
}
if(hMutex != NULL) {
ReleaseMutex(hMutex);
CloseHandle(hMutex);
}
if(hThread != NULL) CloseHandle(hThread);

return result;
}

ここまでは最初に紹介したリンクに書いてあることそのままなので、これで終わるならあちらを読んだ方が面白い。

ここからが問題の64bitプロセスから32bitプロセスのLoadLibraryのアドレスをどうやって知るかなのだが…
はじめはCreateToolhelp32Snapshotでkernel32のベースアドレス取ってきて、PEファイルの構造を読んで関数のRVAを足せば…と試みたのだけれど、ベースアドレスの取得が上手く行かなかった。64bitプロセスから見るとkernel32.dllはロードしておらず、WOW64関連のDLLだけをロードしているように見える。

結局、32bitプロセスを起動して取得するというしょぼい力技で対応することに。もっとスマートな方法あったら教えて欲しい。
PTHREAD_START_ROUTINE pGetProcAddress(BOOL isWow64, const char* funcName, const char* libName = "kernel32"){

if(_stricmp(libName, "user32") != 0 &&
_stricmp(libName, "kernel32") != 0 ) return NULL;

// kernel32, user32.dllの関数はどの全プロセスで同じアドレスにロードされるので
// 自プロセスで取得したアドレスを別プロセスでもそのまま使用できる。
// ただし、プロセスのbit数が異なる場合は別になるため、
// 64bitから32bitのプロセスにアタッチする場合は別の方法でアドレスを取得する。
if(!isWow64)
return (PTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle(libName), funcName);

// 二回目以降の呼び出しはキャッシュを返す。
static map<string, PTHREAD_START_ROUTINE> cache;
string key = string(libName);
key.append(funcName);
if(cache.count(key) ==1) return cache[key];

// 戻り値に32bitでの関数アドレスを返すコマンドを呼び出す。
PROCESS_INFORMATION pi;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.cb=sizeof(si);

// コマンドプロンプトは表示させない。
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;

// このDLLと同じディレクトリ\GetProcAddress32.exe ライブラリ名 関数名
char cmdLine[MAX_PATH + 64 +1];
GetModuleFileName(g_hInst, cmdLine, sizeof(cmdLine));
PathRemoveFileSpec(cmdLine);
PathAppend(cmdLine, "GetProcAddress32.exe");
strcat(cmdLine, " ");
strcat(cmdLine, libName);
strcat(cmdLine, " ");
strcat(cmdLine, funcName);

if(CreateProcess(NULL, cmdLine, NULL,NULL, FALSE,NORMAL_PRIORITY_CLASS, NULL,NULL,&si,&pi) == NULL){
error("CreateProcess()", cmdLine);
return NULL;
}

CloseHandle(pi.hThread);
WaitForSingleObject(pi.hProcess,INFINITE);
DWORD exitCode;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess);

// 実行結果をキャッシュ
PTHREAD_START_ROUTINE addr = (PTHREAD_START_ROUTINE) exitCode;
cache[key] = addr;
return addr;
}

上で呼び出しているコマンド。
#include <windows.h>

int main(int argc, char** argv)
{
if(argc != 3)
return NULL;
else
return (int) GetProcAddress(GetModuleHandle(argv[1]), argv[2]);
}

デバッグ用マクロ (C言語)

デバッグ時のみ有効なエラー出力用マクロ。何となくWindows以外でも動きそうな感じに書いてみたけど、実際に使っているのはWindowsだけ。他のOSは試してないので動かないかもれない。(→追記:一応FreeBSD gcc4.2でそれっぽく動いたのでとりあえず良しとする。)

使い方は共通のヘッダファイルに下を書いておいて、それをincludeして
debug("ProcessId=%d",GetCurrentProcessId());
とか書けば
[D] test.cpp[55] WinMain[0]: ProcessId=6508
みたいにメッセージと一緒にソースコードのファイル名や行数、関数名、エラーコードをコンソールに出力してくれる。
GOTO_Eと書くと、メッセージを吐いてからERROR_HANDLERという名前のラベルにgotoする。
#ifndef _LOG_H
#define _LOG_H

#include <stdio.h>

#ifdef _WIN32
#include <windows.h>
#define ERRCD (int)GetLastError()
#define PATH_DELIM '\\'
#else
#include <stdarg.h>
#include <errno.h>
#include <string.h>
#define ERRCD errno
#define PATH_DELIM '/'
#endif

#ifdef _WIN32
// Ctrl-Cとかでコンソールウインドウを閉じなくする。
inline BOOL WINAPI HandlerRoutine(DWORD type)
{
switch(type)
{
case CTRL_C_EVENT: //Ctrl+C
case CTRL_BREAK_EVENT: //Ctrl+Break
case CTRL_CLOSE_EVENT: //CLOSE
return TRUE;
case CTRL_LOGOFF_EVENT: //LOGOFF
case CTRL_SHUTDOWN_EVENT: //SHUTDOWN
default:
return FALSE;
}
}
#endif

inline void debuglog(const int level, const char *file, const char* func, const int line, const int errcd, const char *fmt, ...)
{
#ifdef _WIN32
static bool hasConsole = false;

if(!hasConsole) {
HWND hWnd = GetConsoleWindow();
if(hWnd == NULL) {
FILE *fp;
AllocConsole();
SetConsoleTitle((LPCSTR)"Debug");
SetConsoleCtrlHandler(HandlerRoutine, TRUE);
freopen_s(&fp, "CONOUT$", "w", stderr);
hasConsole = true;

// 「閉じる」をボタン、右クリックメニューから削除。
// コンソールウインドウを閉じられるとプロセスが終了されるのを防げないため。
hWnd = GetConsoleWindow();
HMENU hMenu = GetSystemMenu (hWnd, FALSE);
HINSTANCE hinstance = (HINSTANCE) GetWindowLongPtr (hWnd, GWLP_HINSTANCE);
DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
DrawMenuBar(hWnd);
}
}
#endif
static char* table[] = {"[D]","[I]","[W]","[E]","[A]"};
#pragma warning(push)
#pragma warning(disable:4996)
static int limitlevel = getenv("loglevel")==NULL?0:atoi(getenv("loglevel"));
#pragma warning(pop)

if(level < limitlevel) return;

// フルパスだったらファイル名に
const char* fname = strrchr(file, PATH_DELIM);
if(fname == NULL)
fname = file;
else
fname++;

va_list argp;
fprintf(stderr, "%s %s[%d] %s[%d]: ", table[level], fname, line, func, errcd);
va_start(argp, fmt);
vfprintf(stderr, fmt, argp);
va_end(argp);
fprintf(stderr, "\n");
}

#ifdef _DEBUG
#define debug(...) debuglog(0, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define info(...) debuglog(1, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define warn(...) debuglog(2, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define error(...) debuglog(3, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define critical(...) debuglog(4, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);

#define GOTO_E {error("[ERROR] goto FUNC_END"); goto FUNC_END;}
#else

#define debug(...)
#define info(...)
#define warn(...)
#define error(...)
#define critical(...)
#define GOTO_E goto FUNC_END
#endif
#endif

2011年9月9日金曜日

lockステートメントは同じスレッドをロックしない (C#)

lockステートメントで少しハマったのでメモ。ハマったコードを単純化したサンプルが以下。こんなプログラム作らねーよというツッコミはなしで。

やってることは単純で起動時にファイルを作って、ウインドウがクリックされたらリネームして元に戻す。リネームして戻すところをlockステートメントで排他をかけている。もし処理が同時に走るとリネーム済みなのに再リネームを試みてFileNotFoundになる。そしてリネームした状態で処理を止められるようポップアップを表示している。

この状態でウインドウを連続クリックすると何が起こるだろうか? 私のようなヘボプログラマは排他待ちになりエラーなしで処理されると直感的に思ってしまった。 でも実はエラーになる。
using System;
using System.IO;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
[DllImport("user32.dll", EntryPoint = "MessageBox")]
extern static Int32 MessageBox(Int32 hWnd, string text, string caption, UInt32 type);

public Form1()
{
InitializeComponent();
if(!File.Exists(fromPath) File.Create(fromPath);
if(File.Exists(toPath) File.Delete(toPath);
}

    string fromPath = "aaa.txt";
    string toPath = "bbb.txt";

    void CriticalSection(string title){
lock (this)
{
File.Move(fromPath, toPath);
MessageBox(0, "処理中", title, 0);
File.Move(toPath, fromPath);
}
}

private void Form1_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
CriticalSection("aaaa");
else
CriticalSection("bbbb");
}
}
}

何故か? ヒントはこちら。

lock によって、あるスレッドがクリティカル セクションになっているときは、別のスレッドはコードのクリティカル セクションにはなりません。ロックされたコードを別のスレッドが使おうとすると、オブジェクトが解放されるまで待機 (ブロック) します。

MSDN lock ステートメント (C# リファレンス)


「別のスレッドは」という表現がこの現象を引き起こした正体。別に同じスレッドが同時には動くことはないんだから問題ないと思うのでは? 上のコードを以下のようにちょっと変えて試してみると良い。
              File.Move(fromPath, toPath);
MessageBox(0, ""+Thread.CurrentThread.ManagedThreadId, title, 0);
File.Move(toPath, fromPath);

同じ数字が表示されるでしょ? 要はクリティカルセクション中でスレッドを切り替えるなということ。スレッドなんて切り替えていないって? 何故、メッセージボックスで処理を止めてるのにメインウインドウの描画が更新されているのか考えてみよう。

アンマネージドコードのデバッグで手を抜いて一時的にメッセージボックスを使ったばかりに変なことにはまって無駄に時間をかけてしまった。

2011年9月6日火曜日

Bloggerの投稿画面は狭すぎる

HDのディスプレイで使っていると狭くてやっていられない。そんな訳で広くする方法を探したわけですが。
見つかったのが以下のユーザスクリプト。


でも、これ古い投稿エディタじゃないと効かない。新しい投稿エディタのメリットより、編集画面が広い方が嬉しいのでしょうがなく設定から古い投稿エディタを有効にして使ってる。画像をアップロードする時に特大に設定ができないのと、プレビューが不完全なのは困るけれど… 他力本願だけど誰か何とかしてくれないだろうか。



設定の保存にxmlファイルを使う(C#)

C#で設定ファイルを簡単に実装しようと思ったらXmlSerializerを利用して設定用クラスの情報をxmlファイルにダンプ、ロードする方法が楽だ。
// 設定用クラス
public Config{
// publicにする必要あり
public string config1 = "初期値";
}

// 起動後
// 設定ファイル読み込み
// Configの部分は設定クラス名で変わる
XmlSerializer serializer = new XmlSerializer(typeof(Config));
string confFile = Path.Combine(Application.StartupPath, Path.GetFileNameWithoutExtension(Application.ExecutablePath) + ".xml");
if (File.Exists(confFile))
{
using (FileStream fs = new FileStream(confFile, FileMode.Open))
{
Config = (Config)serializer.Deserialize(fs);
}
}
// ファイルがない場合は初期値をロード
else
{
Config = new Config();
}

// いろいろな処理をやる。

// 終了前
// 設定ファイルの保存
using (FileStream fs = new FileStream(confFile, FileMode.Create))
{
serializer.Serialize(fs, Config);
}
これだけで動作はするのだが、環境によっては処理がハングすることがあるらしい。標準のXmlSerializerでは実行時に動的コンパイルでシリアライズ処理用のバイナリを作るが、この処理がまずいみたいなので、事前にコンパイル済みのバイナリを用意してそれを利用するようにすれば良いらしい。手順をは以下の通り。
  1. 設定用クラスを含めてコンパイルしたアセンブリからシリアライズ用のDLLを作成する。具体的にはビルドイベントでビルド後のイベントに「"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\sgen.exe" /f "$(TargetPath)" /o:"$(ProjectDir)"」を追加する。
  2. 作成されたDLL(アセンブリ名.XmlSerializers.dll)を参照設定に追加する。
  3. DLLを利用してシリアライズするようにプログラムを修正する。
using Microsoft.Xml.Serialization.GeneratedAssembly;  //先頭に追加

// ConfigSerializerの部分は、設定用のクラス名によって変わる。
// XmlSerializer serializer = new XmlSerializer(typeof(Config));
ConfigSerializer serializer = new ConfigSerializer();
1つだけ自分がハマったことがあったので注意として付記しておく。DLLを参照に追加するとデバック時にDLLが見つからずにエラーにならないよう、ビルドのたびにアセンブリの出力ディレクトリに参照DLLをコピーしてくれるローカルコピーという機能がある。

この際、親切なことに「参照しているDLL名.xml」(&.pdb)という名前のファイルがあればそれもコピーしてくれる。それだけなら歓迎なのだが、何故か「アセンブリ名.xml」のファイルがあれば、一緒にコピーしてしまうという謎の仕様になっているらしい。

何がまずいかというと、このサンプルのように設定ファイル名を「アセンブリ名.xml」にしていて、sgenに/oオプションを付けずアセンブリと同じディレクトリにDLLを出力、それをそのまま参照に追加するという条件が重なると困った現象が起きる。

例えばReleaseディレクトリに出力したDLLを参照に追加したとする。Releaseビルドで一回でもデバッグを実行して設定ファイルを出力した状態で、Debugビルドに切り替えてデバッグを行うと、設定ファイルがReleaseディレクトリからDebugディレクトリにコピーされてしまい、Debugビルドでアプリが書き換えた設定が再ビルドのたびにリセットされてしまうという… これで何時間無駄にしたことか…

対策は以下のどれか
  • DubugビルドとReleseビルドで参照設定の指定をちゃんと切り替える。GUIからは無理なのでプロジェクトファイルをテキストエディタで直接編集する必要あり。
  • ローカルコピーを無効にする。参照設定のプロパティから切り替えられる。
  • 「アセンブリ名.xml」ファイルが出力されない場所にDLLを出力して、参照設定に追加する。
今回の例では3番目を採用した。

コマンドプロンプトで利用可能なフォント

使えるフォントに制限があるみたいなので、使えたやつをメモとして残しておく。(随時更新予定)

◎ 日本語フォント
◎ 英字フォント
  • Lucida Console
  • Courier New
  • Consolas (Vista以降)
→ と思ったけど抜粋して書いておく。

「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Console\TrueTypeFont」に 932. (フォントを2つ以上追加する場合はドットの数を増やしていく)という名前の文字列型のキーを作ってフォント名を設定するとコマンドプロンプトのフォントの設定画面から選べるようになる。

コマンドプロンプトとフォントの怪

コマンドプロンプトの文字化けにしばらく悩まされたのでその奮戦記。

◎ 起こったこと
  • ある日、Visual Studioをインストールするとスタートメニューに入る「Visual Studio 20xx コマンドプロンプト」(VS用の環境変数が設定済みのコマンドプロンプト)を初めて起動したら、日本語が文字化けして表示された。
  • その少し前にmsysgitをインストールしていて、インストール時にコマンドプロンプトのフォント設定に関する確認を求められていたから、それ絡みかもしれないと思いつき、コマンドプロンプトの設定画面からフォントの設定値を見てみるとちゃんと「MSゴシック」になっている。
  • そもそも、msysgitインストール後に普通のコマンドプロンプトを使ったことは既にあって、文字化けは発生していなかった。
はじめはVisual StudioもExpress Editionだし、昔は日本語のサポートが遅れていたし、とVisual Studioのせいかと思って、そちらを想定したキーワードでググってしまい、直ぐに解決しなかった。濡れ衣を着せてごめんなさい。


◎ mysisgitを疑い直してググッてみた

すぐにレジストリ「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Console\TrueTypeFont」のキー「0」を「Lucida Console」→「*MS ゴシック」に変更すれば良いという情報を発見したのだが… ここが長い道のりの始まりだった。
その通りにしようと思いレジストリエディタで「HKEY_LOCAL_MACHINE\~\TrueTypeFont」を開いたら、違和感があって変更できなかったのだ。


違和感の内容は文字化けしているコマンドプロンプトの文字コードはShift-JIS、つまりCP932なのだが、キー「932」の設定値はMSゴシックと、正しそうな設定だということだ。そんな訳でもう少し調べてみることにした。


◎ 調査と実機確認の結果

検索で引っかかった「コンソール(cmd.exe)の文字コードを UTF-8 に」や「MSのナレッジベース」を読んで分かったのは、「HKEY_LOCAL_MACHINE\~\TrueTypeFont」の設定により、コマンドプロンプトのフォント設定画面の一覧に表示されるフォントが決まるということだ。確かにフォント一覧には限られたフォントしか表示されない。

ここから書くことはWebで見つかった断片的な情報と、自分のPC(Windows7 64bit)で色々試した結果から推測したもののため、間違った部分があるかもしれない。

表示するフォントは以下のアルゴリズムで決まる。
  1. コマンドプロンプトの現在のコードページを取得
  2. 各レジストリキーの名前について、以下のチェックを行う。
    1. 名前を先頭からスキャンして数字になる部分を切り出す。
    2. 切り出した文字を数値に変換する。
    3. 現在のコードページが特定のものでなければ、数値に変換した値が0になるキーのフォントが表示される。
    4. ただし、フォントの属性チェックが走り、条件(等幅であるなど)を満たさないフォントは表示されない。うちの環境で表示できたのは「Lucida Console」, 「Consolas」, 「Courier New」だけだった。
    5. 現在のコードページが特定のものである場合は、数値に変換した値がコードページと同じキーのフォントが表示される。特定のコードページというのはおそらくレジストリに初めからエントリされている932, 936, 949, 950でおそらく漢字圏のコードページだけの特別処理なのだろう。
    6. これも、フォントの属性チェックが走るが、0の場合と少し条件が異なる。表示ができたのは「MS ゴシック」, 「Osaka-等幅」, 「VL ゴシック」などの日本語フォントのみだった。(試してないがWebの情報だとMeiryoKe_Consoleもいけるらしい)
  3. 例外としてラスタフォント(Terminal)は必ず表示する。
複雑で例がないと分かり難い。レジストリが
00          フォントA
000 フォントB
932    フォントC (Shift-JIS)
932.    フォントD
932a フォントE
936    フォントF
65001 フォントG (UTF-8)
だったとすると、コードページが932であればフォントC, D, Eが表示される。コードページ936ならフォントF。ここまでは直感的なのだが、コードページ65001はフォントA, Bが表示されるわけでWeb上にも困惑している人がちらほら。


◎ ややこしいことに…

上で決まるのはあくまでコマンドプロンプトの設定画面から設定できるフォントであって、直接設定の保存先を書き換えれば、設定画面に表示できないフォントも設定できる。では、設定の保存先がどこなのかというとこれがまたややこしい。

まず、大元の設定は「HKEY_CURRENT_USER\Console」にある。素の状態でコマンドプロンプトを使うと、ここの設定が読み込まれる。ただ、ここの設定は読み込み専用で、コマンドプロンプトの設定画面から設定を変更しても書き込まれない。
変更のあった設定値だけ、別の場所に書き込まれてそちらが優先して読まれる。

その書き込み先というのはコマンドプロンプトをショートカットから起動した場合はショートカットファイル、exeを直接起動した場合は「HKEY_CURRENT_USER\Console」の下にディレクトリが掘られ、その中に保管される。exeが変わると作られるディレクトリ名が変わる。例えば64bit版Windowsだと、32bitと64bitの2つのcmd.exeがあるが、ディレクトリ名はそれぞれ「%SystemRoot%_System32_cmd.exe」, 「%SystemRoot%_SysWOW64_cmd.exe」となる。

UTF-8で設定画面からは選べない日本語フォントを設定したい場合、ここの設定値を変えれば良い。ショートカットの書き換えは難しいのでレジストリを書き換える。実はexeを直接起動した場合に作られるディレクトリはexeのパスで決まるわけではなく、起動時のウィンドウタイトルで決まり、これはstartコマンド指定できる。手順は以下。
  1. cmd.exeのショートカットを作成する。
  2. リンク先を「cmd.exe /c start "utf8" cmd」に変更する。
  3. そのリンクからコマンドプロンプを起動して、フォントの設定変更を行う。この時ボールドフォントにチェックを入れておくと良い。(後述)
  4. utf8以下に新しい設定値が作られるので、それを書き換える。注意点としてフォントの設定はFaceName, FontSize, FontWeightの3つが揃っていないと不正と判断され、親ディレクトリの設定が読み込まれてしまう。項番3でフォントをボールドフォントにしないとFontWeightというキーが作られないため、このディレクトリのフォント設定は読まれない。別に自分でキーを追加しても可。
  5. ついでにCodePageというキーを65001にしておけば起動毎のchcpが不要になる。

注意点としては、やはり任意のフォントが設定できるわけではなくコードページ932で一覧に表示できないフォントは指定しても表示がおかしくなるということと、以降コマンドプロンプトの設定画面を使おうとしても日本語フォントは選べないため、regeditを使用するなくなってしまうということだ。


◎ あの日、私に何が起こっていたか?

私はmsysgitをインストールする前の時点でコマンドプロンプトを使ってフォントの設定を書き換えたことがあり、ショートカットにその時設定されたフォントが残っていた。でも、Visual Studioの管理コンソールは使ったことがなかったのでショートカットにフォントの設定は残っておらず、レジストリキーのデフォルトの設定「HKEY_CURRENT_USER\Console\FaceName」が読まれた。ここの設定がmsysgitで書き換えられたので文字化けが発生していたというわけである。

コマンドプロンプトのフォントの設定画面ではフォントの表示はMSゴシックになっていたが、それはフォント選択リストの仕様によりコードページ932では「Lucida Console」は表示できなかったためであり、実際の設定値は「Lucida Console」になっていたのだ。だから改めてフォントを設定しなおせば文字化けは治っていたはずである。


◎ 最後に残った謎

色々試して納得しかかったけれど、よくよく考えると1つだけ説明のつかないことがある。それは何故ググったらmsysgitで文字化けしたら「HKEY_LOCAL_MACHINE\~\TrueTypeFont」を直せという情報が引っかかるのかだ。実際自分の環境ではここを直さなくても文字化けは治ったし、調査結果からは治るとも思えない。OSバージョンのせいかとも思ったけれど情報元の人もWindow7(64bit)だし、複数いるようだし、勘違いということもなさそうだ。

自分的には正しい対策は「HKEY_CURRENT_USER\Console\FaceName」を書き換えることのはずなので納得が行かない。でももう調べる気力は起きない。もう既に困らなくなる程度に情報が得られてからも突っ込んで調べ過ぎた感がある。
教えてエロイ人。


Bloggerでラベルのリネーム

一度つけたラベルをリネームしたくなり、メニューを見てみたけどそれらしいものがない。どうしようと思ってググッてみたら、原始的な方法で実現できることが分かった。

http://kamimura4401.blogspot.com/2007/01/blogger-tips.html

投稿の一覧でリネームしたいラベルを選んで一覧表示して、全選択して新しい名前のラベルを追加して古いラベルを削除でOK。ラベルの付いた記事が多くなり複数ページに表示されるようになるとちょっと面倒になるかもしれないが、できなくはなさそうだ。分かってみれば何故すぐに思いつかなかったんだろうと思う。

2011年9月3日土曜日

シリアライズ可能なDictionary(C#)

Dictionaryはシリアライズできないのでシリアライズ可能なSerializableDictionaryを作る方法。

http://d.hatena.ne.jp/lord_hollow/20090206
http://d.hatena.ne.jp/lord_hollow/20090602/p1

上に書いてある方法だと、Dictionaryが空の時にエラーになるので少し手(色付け)を入れてる。
public class SerializableDictionary<Tkey, Tvalue> : Dictionary<Tkey, Tvalue>, IXmlSerializable
{
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        XmlSerializer serializer = new XmlSerializer(typeof(KeyValue));

bool isEmpty = reader.IsEmptyElement; reader.Read(); if (isEmpty) { return; }
while (reader.NodeType != System.Xml.XmlNodeType.EndElement) { KeyValue kv = serializer.Deserialize(reader) as KeyValue; if (kv != null) Add(kv.Key, kv.Value); } reader.Read(); } public void WriteXml(System.Xml.XmlWriter writer) { XmlSerializer serializer = new XmlSerializer(typeof(KeyValue)); foreach (var key in Keys) { serializer.Serialize(writer, new KeyValue(key, this[key])); } } public class KeyValue { public KeyValue() { } public KeyValue(Tkey key, Tvalue value) { Key = key; Value = value; } public Tkey Key { get; set; } public Tvalue Value { get; set; } } }

2011年9月1日木曜日

ListViewのちらつきを抑える(C#)

ListViewはダブルバッファリングが無効なため、項目を追加・削除すると表示にちらつきが生じる。また、背景色を変更するとListView全体に再描画され、これもまたちらつく。一定時間おきに背景色を更新するアプリを作ったら気になってしょうがなかった。対策にはListViewを継承したクラスを作って代わりに使用すれば良い。

引用元:http://geekswithblogs.net/CPound/archive/2006/02/27/70834.aspx
class ListViewNF : System.Windows.Forms.ListView
{
public ListViewNF()
{
//Activate double buffering
this.SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint, true);

//Enable the OnNotifyMessage event so we get a chance to filter out
// Windows messages before they get to the form's WndProc
this.SetStyle(ControlStyles.EnableNotifyMessage, true);
}

protected override void OnNotifyMessage(Message m)
{
//Filter out the WM_ERASEBKGND message
if(m.Msg != 0x14)
{
base.OnNotifyMessage(m);
}
}
}