Win32のバージョンリソース

EXEファイルにバージョンリソースを書き込むコードを書こうとググって見たところ、意外にもあまりまともな記事が見つからなかったので、自分で書いてみた。基本的には、VERSIONINFO ResourceからMSDNの記事をたどって、全体構造を把握するだけなんだけれども、何で、こんな基本的な事を手でやらないといけないのかはよく分からない。また、一部の数字は、実際のファイルをダンプして得られました。つまり、根拠が分からないものがあります。特に最後の2バイトとか。

いずれにしても、面倒なことは面倒なので、簡単なラッパークラスをつくった。ATLを使って手抜きをしているのはご愛敬。基本的には、CStringT::Formatと、CT2Wが使いたかっただけ。全部、_UNICODE/UNICODEでいいのであれば、適当に置換すれば使えます。

#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <vector>
#include <map>
#include "atlbase.h"
#include "atlstr.h"

class VersionInfoBuilder
{
public:
  // Lang-ID: 0x409=英語(米国)
  VersionInfoBuilder(WORD langID = 0x409) : _langID(langID)
  {
    _fileVer[0] = _fileVer[1] = _fileVer[2] = _fileVer[3] = 0;
    _prodVer[0] = _prodVer[1] = _prodVer[2] = _prodVer[3] = 0;
  }

  void setProp(LPCTSTR name, LPCTSTR val)
  {
    _props[(LPCWSTR)CT2W(name)] = (LPCWSTR)CT2W(val);
  }
  
  void setFileVersion(WORD a, WORD b, WORD c, WORD d)
  {
    _fileVer[0] = a;
    _fileVer[1] = b;
    _fileVer[2] = c;
    _fileVer[3] = d;
  }

  void setProdVersion(WORD a, WORD b, WORD c, WORD d)
  {
    _prodVer[0] = a;
    _prodVer[1] = b;
    _prodVer[2] = c;
    _prodVer[3] = d;
  }
  
  void build(std::vector<BYTE>& buffer)
  {
    buildVerInfo();
    _buffer.swap(buffer);
  }
  
  bool updateExeFile(LPCTSTR fileName)
  {
    std::vector<BYTE> data;
    build(data);
    
    HANDLE hUpdate = BeginUpdateResource(fileName, FALSE);
    if(!hUpdate)
      return false;
    
    BOOL ok = UpdateResource(
      hUpdate, RT_VERSION, MAKEINTRESOURCE(VS_VERSION_INFO), _langID,
      &data[0], data.size());
    
    return EndUpdateResource(hUpdate, ok ? FALSE : TRUE) ? true : false;
  }

private:
  static const WORD CODEPAGE_UCS2LE = 0x4b0;
  WORD _langID;
  WORD _fileVer[4], _prodVer[4];
  typedef std::map<CStringW, CStringW> StrMap;
  StrMap _props;
  std::vector<BYTE> _buffer;
  
  void clear()
  {
    _buffer.clear();
  }
  
  void writeWord(size_t ptr, WORD w)
  {
    _buffer[ptr] = (BYTE)(w & 255);
    _buffer[ptr + 1] = (BYTE)(w >> 8);
  }
  
  size_t pushWord(WORD w)
  {
    _buffer.push_back(w & 255);
    _buffer.push_back(w >> 8);
    return _buffer.size() - 2;
  }
  
  size_t pushByte4(BYTE a, BYTE b, BYTE c, BYTE d)
  {
    _buffer.push_back(a);
    _buffer.push_back(b);
    _buffer.push_back(c);
    _buffer.push_back(d);
    return _buffer.size() - 4;
  }
  
  void align4()
  {
    size_t pos = _buffer.size() & 3;
    if(pos == 0)
      return;
    size_t padLen = 4 - pos;
    static const BYTE pads[] = {0, 0, 0, 0};
    _buffer.insert(_buffer.end(), pads, pads + padLen);
  }
  
  size_t pushStr(LPCWSTR str)
  {
    size_t pos = getPtr();
    for(;;)
    {
      WORD w = *str++;
      pushWord(w);
      if(!w)
        break;
    }
    align4();
    return pos;
  }
  
  size_t getPtr() const
  {
    return _buffer.size();
  }
  
  void writeLength(size_t ptr)
  {
    writeWord(ptr, getPtr() - ptr - 2);
  }
  
  CStringW verStr(const WORD* v) const
  {
    CStringW ver;
    ver.Format(L"%u, %u, %u, %u", v[0], v[1], v[2], v[3]);
    return ver;
  }

  void buildVerInfo()
  {
    _props[L"FileVersion"] = verStr(_fileVer);
    _props[L"ProductVersion"] = verStr(_prodVer);
    
    clear();
    
    size_t ptrWholeSize = pushWord(0); // whole size (dummy)
    pushWord(0x34);
    pushWord(0x00);
    pushStr(L"VS_VERSION_INFO");
    pushByte4(0xbd, 0x04, 0xef, 0xfe); // Signature BD04EFFE
    pushByte4(0, 0, 1, 0); // struct version
    pushWord(_fileVer[1]);
    pushWord(_fileVer[0]);
    pushWord(_fileVer[3]);
    pushWord(_fileVer[2]);
    pushWord(_prodVer[1]);
    pushWord(_prodVer[0]);
    pushWord(_prodVer[3]);
    pushWord(_prodVer[2]);
    pushByte4(0x17, 0, 0, 0); // file flags mask (0x17)
    pushByte4(0, 0, 0, 0); // file flags (0)
    pushByte4(4, 0, 0, 0); // OS: 32-bit Windows (even on x64)
    pushByte4(1, 0, 0, 0); // file type: App(1)
    pushByte4(0, 0, 0, 0); // sub-file type: Unknown(0)
    pushByte4(0, 0, 0, 0);
    pushByte4(0, 0, 0, 0);
    
    size_t ptrStrFileInfoSize = pushWord(0);
    pushByte4(0, 0, 1, 0);
    pushStr(L"StringFileInfo");
    
    size_t ptrStrTable = pushWord(0);
    pushByte4(0, 0, 1, 0);
    
    CStringW loc;
    loc.Format(L"%04x%04x", _langID, CODEPAGE_UCS2LE);
    pushStr(loc);
    
    for(StrMap::const_iterator it = _props.begin();
      it != _props.end();
      ++it)
    {
      size_t ptr = pushWord(0);
      size_t ptrValLen = pushWord(0);
      pushWord(1);
      pushStr(it->first);
      size_t ptrVal = getPtr();
      pushStr(it->second);
      size_t vsize = getPtr() - ptrVal;
      writeWord(ptrValLen, vsize / 2);
      writeLength(ptr);
    }
    
    writeLength(ptrStrTable);
    writeLength(ptrStrFileInfoSize);
    
    pushWord(0x44);
    pushWord(0);
    pushWord(1);
    pushStr(L"VarFileInfo");
    pushWord(0x24);
    pushWord(4);
    pushWord(0);
    pushStr(L"Translation");
    pushWord(_langID);
    pushWord(CODEPAGE_UCS2LE);
    pushWord(0x613c); // ???
    
    writeLength(ptrWholeSize);
  }
};

int _tmain(int argc, TCHAR* argv[])
{
  VersionInfoBuilder vib(0x409);

  // それぞれのプロパティを直接設定する
  vib.setProp(_T("Comments"), _T("Comments!!!"));
  vib.setProp(_T("CompanyName"), _T("CompanyName!!!"));
  vib.setProp(_T("FileDescription"), _T("FileDescription!!!"));
  vib.setProp(_T("InternalName"), _T("InternalName!!!"));
  vib.setProp(_T("LegalCopyright"), _T("LegalCopyright!!!"));
  vib.setProp(_T("LegalTrademarks"), _T("LegalTrademarks!!!"));
  vib.setProp(_T("OriginalFilename"), _T("OriginalFilename!!!"));
  vib.setProp(_T("PrivateBuild"), _T("PrivateBuild!!!"));
  vib.setProp(_T("ProductName"), _T("ProductName!!!"));
  vib.setProp(_T("SpecialBuild"), _T("SpecialBuild!!!"));
  vib.setFileVersion(1, 2, 3, 4);
  vib.setProdVersion(5, 6, 7, 8);
  
  // 指定されたファイルに埋め込む
  vib.updateExeFile(argv[1]);

  return 0;
}

ボリュームがマウントされているパスの一覧を取得する

FileSystemWatcherとか、ReadDirectoryChangesWは、ファイルの更新とかを監視するには便利な関数ですが、これらでサブディレクトリを監視するようにしても、サブディレクトリがマウントポイントで、他のボリュームが接ぎ木されているような場合にはそのマウントポイントより下に関しては、監視が出来ません。そのため、隈無く監視の目を光らせるためには、すべてのマウントポイントを確認する必要があります。

ということで、その一覧を列挙する実験。

#include <windows.h>
#include <stdio.h>
#include <locale.h>
#include <tchar.h>

bool enumMountPoints(LPCTSTR volume)
{
  _tprintf(_T("%s\n"), buf);

  DWORD dwSize;
  if(!GetVolumePathNamesForVolumeName(volume, NULL, 0, &dwSize) && GetLastError() != ERROR_MORE_DATA)
    return false;
  
  TCHAR* buf = new TCHAR[dwSize];
  if(!GetVolumePathNamesForVolumeName(volume, buf, dwSize, &dwSize))
  {
    delete[] buf;
    return false;
  }
  
  static const LPCTSTR types[] = {_T("UNKNOWN"), _T("INVALID"), _T("REMOVABLE"), _T("FIXED"), _T("REMOTE"), _T("CDROM"), _T("RAMDISK")};
  
  for(TCHAR* p = buf; *p;)
  {
    size_t len = lstrlen(p);
    if(len)
    {
      DWORD type = GetDriveType(p);
      _tprintf(_T("  %s : %s\n"), (type <= DRIVE_RAMDISK) ? types[type] : _T("?"), p);
    }
    p += len;
  }
  delete[] buf;
  return true;
}

int _tmain(int argc, TCHAR* argv[])
{
  setlocale(LC_ALL, "");
  
  TCHAR buf[MAX_PATH];
  HANDLE hFind = FindFirstVolume(buf, MAX_PATH);
  if(hFind == INVALID_HANDLE_VALUE)
    return 0;
  
  do
  {
    enumMountPoints(buf);
  }
  while(FindNextVolume(hFind, buf, MAX_PATH));
  FindVolumeClose(hFind);
  
  return 0;
}

僕のマシンでの実行結果:

\\?\Volume{2bcfbee6-8376-11de-a1bf-806e6f6e6963}\
\\?\Volume{54a83329-9c55-11de-848d-806e6f6e6963}\
  FIXED : C:\work
\\?\Volume{2bcfbee7-8376-11de-a1bf-806e6f6e6963}\
  FIXED : C:\
\\?\Volume{bd99cfb5-837f-11de-8aca-00125a59038a}\
  FIXED : J:\
\\?\Volume{90f63e07-a90c-11de-bfbe-806e6f6e6963}\
  CDROM : D:\
\\?\Volume{938e7b17-8378-11de-88b6-00125a59038a}\
  CDROM : E:\

マウントポイントもちゃんと列挙されているし、GetDriveTypeでちゃんとマウントポイントの種類まで分かる。ちなみに、workにマウントされているのはSSD。最初のボリュームは、Windows 7のシステムパーティションか。

C#で全ボリューム(ただしFixedなものに限るを監視)

上記を踏まえた上で、全ボリュームを2分間監視するサンプル(新規に作成されたファイルonly)。

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;

public class WatchAllVolumes
{
  public static void Main(string[] args)
  {
    var wfList = new List<FileSystemWatcher>();
    
    foreach (string root in getAllMountPointsForFixedDrives())
    {
      var fsw = new FileSystemWatcher();
      fsw.Path = root;
      fsw.IncludeSubdirectories = true;
      fsw.Filter = "*";
      fsw.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.FileName | NotifyFilters.DirectoryName;
      fsw.Created += new FileSystemEventHandler(onCreated);
      fsw.Changed += new FileSystemEventHandler(onCreated);
      fsw.Renamed += new RenamedEventHandler(onCreated);
      fsw.EnableRaisingEvents = true;
      wfList.Add(fsw);
      Console.WriteLine("Watching: {0}", root);
    }
    
    // sleep for 2 minutes
    System.Threading.Thread.Sleep(1000 * 120);
    
    foreach (FileSystemWatcher fsw in wfList)
      fsw.Dispose();
  }
  
  static void onCreated(object sender, System.IO.FileSystemEventArgs e)
  {
    Console.WriteLine("{0}", e.FullPath);
  }
  
  static string[] getAllMountPointsForFixedDrives()
  {
    var mp = new List<string>();
    var sb = new StringBuilder(MAX_PATH);
    IntPtr hFind = FindFirstVolume(sb, MAX_PATH);
    if (hFind == INVALID_HANDLE_VALUE)
      return mp.ToArray();
    try
    {
      do
      {
        string volume = sb.ToString();
        if (GetDriveType(volume) == DriveType.Fixed)
        {
          string path = getFirstMountPointsForVolume(volume);
          if (!string.IsNullOrEmpty(path))
            mp.Add(path);
        }
        sb = new StringBuilder(MAX_PATH);
      }
      while(FindNextVolume(hFind, sb, MAX_PATH));
      return mp.ToArray();
    }
    finally
    {
      FindVolumeClose(hFind);
    }
  }
  
  private static string getFirstMountPointsForVolume(string volume)
  {
    int size;
    GetVolumePathNamesForVolumeName(volume, null, 0, out size);
    char[] buf = new char[size];
    GetVolumePathNamesForVolumeName(volume, buf, size, out size);
    
    foreach (string s in new string(buf).Split('\0'))
      if (!string.IsNullOrEmpty(s))
        return s; // return the first mount point
    return string.Empty;
  }
  
  static readonly int MAX_PATH = 260;
  static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
  
  enum DriveType
  {
    Unknown = 0,
    NoRootDir = 1,
    Removable = 2,
    Fixed = 3,
    Remote = 4,
    CdRom = 5,
    RamDisk = 6
  }
  [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  static extern DriveType GetDriveType(string lpRootPathName);
  [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  static extern IntPtr FindFirstVolume(System.Text.StringBuilder longPath, int bufSize);
  [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  static extern bool FindNextVolume(IntPtr hFind, System.Text.StringBuilder longPath, int bufSize);
  [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  static extern bool FindVolumeClose(IntPtr hFind);
  [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
  static extern bool GetVolumePathNamesForVolumeName(string lpszVolumeName, char[] lpszVolumePathNames, int cchBuferLength, out int lpcchReturnLength);

}

extern "C++"

Windows Driver Kit for Windows 7 and Windows Server 2008 R2

をよく見ると、

extern "C++"

というのがある。これは何ですか・・・。理屈から言うと、

extern "C"
{
// ここはC

  extern "C++"
  {
    // ここはC++
  }

}

という感じで、入れ子にして、Cのコードの中でも、C++のコードを使いたい場合にはつじつま合わせとしては必要になる。ただ、普通に考えて、これが必要なのは、

extern "C" {
#include "someheader.h"
}

といった感じで、このヘッダ、C++対応できてないから、Cのリンケージ指定しちゃえ的な扱いを受けたヘッダだけだと思われる。

で、これは、マイクロソフトの独自拡張?

Using extern to Specify Linkage (Visual C++ Language Reference)

には、単に、

Microsoft C++ supports the strings "C" and "C++" in the string-literal field. All of the standard include files use the extern "C" syntax to allow the run-time library functions to be used in C++ programs.

としか書いてない。
まぁ、普通は使うことはないからいいか。

超例外=メタデータ=コードが速くなるぜ理論

例外原理主義 - TrickDiary

超例外原理主義

このナンセンス感、嫌いじゃないです。

ただ、最近、何もかもがメタデータ(属性・アノテーション)に見える僕にとっては、別のアプローチの方がおもしろいのではないかと思う部分でもあります。
個人的には、assertと例外の関連性あたりから攻めていきたいテーマです。

assert

assertとはいわば、メタデータであるというのが僕の認識です。下手をすると、コメントに成り下がりますが、それでもdoxygenで生成されるドキュメント程度の意味はあるでしょう。

switch(a)
{
  case 0:
    something();
    break;
  case 1:
    theOtherThing();
    break;
  default:
    ASSERT(0); // ここには来ない。はず。
}

といったコードを書く人の気持ちは分かりません。しかしながら、

#define ASSERT(e) (__assume(e))

などと定義されているのであれば話は別です。これは、ASSERTが__assumeというコンパイラの最適化に利用できる可能性のあるメタデータに変身するからです。

一方で、ライブラリ如きがメインプログラムの意志に反して勝手に死ぬの禁止教(別名:abortすんな教)の信者である僕にとっては、

#define ASSERT(cond) throw new std::runtime_error(# cond)

なんてコードには何の抵抗も感じません。こっちだと上位のコードでフックできる可能性が残されるので、安心して寝ることが出来ます。

例外

少しだけ話をすり替えて、Javaの話にすると、Javaでは、メソッドが例外を放つ場合、そのメソッドには必ず発生する可能性のある例外を列記しなければなりません。

void someMethod() throws FooBarException
{
  ...
}

つまり、この時点で、コンパイラは、このメソッドが陥る例外状態に関してのメタデータを得ることが出来ると言えます。現状のコンパイラがこの情報をどの程度有効に使っているのかについては一切知りませんが、起きうる実行時の問題について、事前に情報を得られることは、何らかの実行時最適化に寄与できる可能性があるでしょう。

まぁ、Javaに関して言えば、こんな風に記述しなくても、プログラムを見れば起きうる例外状態は分かるだろうという話はあります。実際、throwsをちゃんと書かないとエラーになりますし。

また、C++でも例外処理に関しては、いわゆる例外フレーム(g++がらみだとこういう悲しい話もある)というものを作成するので、そういう意味では、別にJavaに限らず、バイナリコードの中に例外は深く刻み込まれるのです。

エラーコード

そして、私が目の敵にするのがいわゆる、エラーコードと言われる物です。理由は簡単で、コードがバケツリレーになってしまうからです。

ErrorCode someFunc()
{
  ErrorCode err = anotherFunc();
  if(err != Success)
    return err; // バケツリレー

  ...
  return Success;
}

ErrorCode anotherFunc()
{
  ErrorCode err = yetAnotherFunc();
  if(err != Success)
    return err; // バケツリレー

  ...
  return Success;
}

ErrorCode yetAnotherFunc()
{
  AbsolutelyDifferentFunc(); // あぁ、エラー処理が面倒になってきた・・・
  return Success;
}

この様な雑多なif文はプログラマを憂鬱にするだけでなく、プログラマがエラー処理を省略するという最悪の結末を招くきっかけとなります。

ただし、エラーコードのスキームのまずさは、もっと別の部分にあります。それは、エラー処理がifによって行われる場合、if文のthenとelse、どちらが主処理で、どちらが例外的な処理なのかが、簡単には分からなくなると言うことです。
これは例によって、UNIX系では、-1がエラーで、0が正常終了などの関数が多く、Windowsでは、戻り値がBOOLの関数が多いなど、文化的な背景を理解すればなんとなく分かるという部分はありますが、それでも、

if(!someFunction())
{
  // ケースA
}
else
{
  // ケースB
}

といったコードで、どちらが主処理といったことを知るには、コードの詳細をチェックする以外にないでしょう。

そして、さらに悪いことに、このようなif文は、最近の高度な分岐予測、投機的実行の機能を持つCPUを容易に攪乱することになります。プログラム言語としては、何らかの形で、どっちが主処理なのかをコンパイラ、CPUに明示できると良い部分でしょう。

ところが、このコードを例外をもって書き換えれば、

if(!someFunction())
{
  // ケースA
}
else
{
  // ケースB
  throw std::exception("不味いことが起きたよー!");
}

これは、コンパイラにとっても、ケースBが例外的であり、ケースAが主処理であることはある程度簡単に類推することが可能になります。

例外でも、内部的にはifによって処理されているじゃないかという議論はあるでしょうが、例外処理によって生成される条件分岐のインストラクションに関しては、コンパイラがバイナリコードに「これは例外処理である(分岐予測では分岐予想率を主処理に比べて著しく下げる)」というメタデータを埋め込むことによって、CPUを無駄に混乱させることを防ぐことは十分に可能でしょう。

結論

結局、(少なくとも私には、)assert・例外はメタデータであるというのが結論であるように見えます。雑多なエラー処理コードのifからは、それが例外的な処理であると行った情報を自動的に得ることは難しいでしょう。しかしながら、例外処理を使うことによって、コンパイラがバイナリコードにCPUの分岐処理に有効かもしれないメタデータを追加できる可能性を持たせることが出来るわけです。

そして、この帰結からは、逆説的に、例外を乱用すべきでない理由も見えてきます。それは、「例外処理」はCPUに対して、例外的な処理であると通知されるわけですから、本質的に例外ではない処理に例外を使うことは避けるべきでしょう。

例外が静的なメタデータに過ぎないと考えると、C++ templateのような静的な解析によるコード処理にブレイクスルーのようなものが来ないかなぁ・・・・まぁ、来ないだろうなぁなどと妄想も出来るんじゃないかと常々考えているわけです。

ユーザーに「サービスとしてログオン」権限を付与する

特定のサービスをユーザーアカウントで起動しようとすると、ユーザーにサービス権限がないことが問題になることがある。特にVistaでは、Administratorsに所属していてもこの権限がないので、いろいろとやっかいだ。

これを解決するために、

// espresso3389というユーザーにサービス起動の権限を付与する
addSeServiceLogonRightToUser("espresso3389");

という感じで使える関数を作成してみた。

#include <windows.h>
#include <sddl.h>
#include <iads.h>
#include <ntsecapi.h>

#include <vector>

#include "atlbase.h"
#include "atlstr.h"

using namespace ATL;

// アカウント名からSIDと所属ドメイン名を取得する
PSID lookupAccountName(LPCTSTR pszName, CString& domainName, std::vector<BYTE>& buf)
{
  DWORD domNameSize = 0;
  DWORD dwSidSize = 0;
  SID_NAME_USE snu;
  LookupAccountName(NULL, pszName,
    NULL, &dwSidSize, NULL, &domNameSize, &snu);
  if(dwSidSize && domNameSize)
  {
    buf.resize(dwSidSize);
    PSID psid = (PSID)&buf[0];
    LookupAccountName(NULL, pszName,
      psid, &dwSidSize,
      domainName.GetBufferSetLength(domNameSize), &domNameSize, &snu);
    return psid;
  }
  return NULL;
}

// 指定のユーザーに指定の権限を付与する
bool addAccountRight(LPCTSTR pszName, LPCTSTR right)
{
  std::vector<BYTE> buf;
  CString domainName;
  PSID psid = lookupAccountName(pszName, domainName, buf);
  if(!psid)
  {
    return false;
  }

  CStringW rightW = CT2W(right);
  LSA_HANDLE handle;
  LSA_OBJECT_ATTRIBUTES objAttr;
  ZeroMemory(&objAttr, sizeof(objAttr));
  NTSTATUS status = LsaOpenPolicy(
    NULL, &objAttr, POLICY_ALL_ACCESS, &handle);
  if(status)
  {
    SetLastError(LsaNtStatusToWinError(status));
    return false;
  }

  USHORT size = (USHORT)rightW.GetLength() * 2;
  LSA_UNICODE_STRING rights = {size, size, (LPWSTR)(LPCWSTR)rightW};
  status = LsaAddAccountRights(handle, psid, &rights, 1);
  if(status)
  {
    DWORD err = LsaNtStatusToWinError(status);
    LsaClose(handle);
    SetLastError(err);
    return false;
  }
  LsaClose(handle);
  return true;
}

// 指定のユーザーにSeServiceLogonRightを付与する
bool addSeServiceLogonRightToUser(LPCTSTR pszName)
{
  return addAccountRight(pszName, _T("SeServiceLogonRight"));
}

後に気づいたが、同様のコードが、KB132958: How To Manage User Privileges Programmatically in Windows NTにある。