はじめに

2018年12月6日(金)に開催の第36回デベロッパーキャンプではT5セッション「事例で学ぶDelphi / C++Builder開発手法」で技術的なコメントを述べさせて頂きましたが、その際にC++Builderの32ビットアプリケーションを64ビット化する場合には、細々とした注意点があるとご説明しました。
その注意点について、このブログ記事でフォローアップさせていただきます。
なお、T5のセッションのビデオ視聴や資料のダウンロードは下記のURLより行なえます。(公開され次第追記します)

アライメントの違いに注意する

32-bit向けのアプリを64-bitコンパイラで単にビルドするとアライメントの不整合によってデータが正しく扱えない場合があります。この代表的な例はC言語の構造体をデータの入出力に用いている場合です。

C++言語はC言語がベースですが、C言語は「高級アセンブラ」と称されるくらいにCPUのアーキテクチャに依存したコードを記述できます。この特徴はC++言語にも引き継がれています。特にポインタやビット演算はその最たる部分です。またC言語はC++言語と互換性が高いので、C言語で書かれたコードをC++言語のプロジェクトで再利用するのは良くある手法です。(過去に自分も何度も経験があります)
またデータをファイルへ入出力するときに構造体で持つデータのイメージをそのままファイルに入出力するという実装も良く用いられます。
例えば、Windowsのビットマップファイル(*.bmp)が有名な例です。
では、同じ構造体を32-bitと64-bitで扱う場合にアライメントがどのように変わるかを実験をしてみましょう。たとえばこのような構造体があります。

struct MyRecord {
  char ch;
  long ID;
  char name[10];
  long double x;
  long double y;
  float h;
};

この構造体の情報をC++Builderのアプリケーションフォームに表示させることにします。TButtonとTMemoを貼り付けて、TButtonのクリックでTMemoに構造体のアライメントを出力するようにしましょう。

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  MyRecord rec;

  Memo1->Clear();
  Memo1->Lines->Add(L"Size = " + UnicodeString(sizeof(rec)));
  Memo1->Lines->Add(L"long Size = " + UnicodeString(sizeof(rec.ID)));
  Memo1->Lines->Add(L"long double Size = " + UnicodeString(sizeof(rec.x)));
  Memo1->Lines->Add(L"Offset name = " + UnicodeString(offsetof(MyRecord, name)));
  Memo1->Lines->Add(L"Offset x = " + UnicodeString(offsetof(MyRecord, x)));
  Memo1->Lines->Add(L"Offset y = " + UnicodeString(offsetof(MyRecord, y)));
  Memo1->Lines->Add(L"Offset h = " + UnicodeString(offsetof(MyRecord, h)));
}

では、ビルドターゲットをWindows 32ビットとWindows 64ビットの2種類で切り替えてビルドし、TMemoに出力される結果を確認しましょう。

32-bitの場合は次のように出力されます。

Size = 56
long Size = 4
long double Size = 10
Offset name = 8
Offset x = 24
Offset y = 40
Offset h = 52

これに対して64-bitの場合は次のように出力されます。

Size = 48
long Size = 4
long double Size = 8
Offset name = 8
Offset x = 24
Offset y = 32
Offset h = 40

同じソースコードでも、ビルドターゲットの違いにより結果が一致しないことが確認できました。
アライメントが異なりますし、構造体自体のサイズも異なっています。long doubleに着目してみましょう。32-bitでは10バイトであるのに対し、64-bitでは8バイトと小さくなっていることが分かります。
もし、この構造体をそのままファイルに入出力していると、32-bit版と64-bit版のアプリでは同じデータファイルを扱うことができなくなります。
この例は非常に極端ですが、昔からの「秘伝のコード」を使っていると良くあることです。
アプリケーションの64-bit化では、単にデータ型の変換に注意するだけではなく、このような部分を精査して適切なリファクタリングを実施することが必要です。

NO_STRICTマクロの削除による厳密な型チェックの実施

古いC++Builderのバージョンでは、Windowsハンドルなどの関係で、マクロNO_STRICTを有効にしてメソッド呼び出しの厳密な型チェックを行っていない場合がありました。
10.3 Rioでは、この定義自体が削除されます。このことによりコンパイル時に厳密な型チェックが実施され、メソッドや関数呼び出し時の型安全性が担保されるようになります。

http://docwiki.embarcadero.com/RADStudio/Rio/ja/C%2B%2B_%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A7%E3%81%AE%E5%8E%B3%E5%AF%86%E3%81%AA%E5%9E%8B%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%AE%E4%BD%BF%E7%94%A8

#pragma link で参照するライブラリの拡張子が異なる

DLLのインポートライブラリなどをプロジェクトファイルで指定する代わりに、ソースコードから #pragma linkで利用するテクニックがあります。

#pragma link "XercesLib.lib"

64ビット版ではライブラリの拡張子が".lib"ではなく、".a"になるので、#pragma linkの見直しも必要です。

http://docwiki.embarcadero.com/RADStudio/Rio/ja/Pragma_link

おまけ?

?????

//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop
#include <tchar.h>
//---------------------------------------------------------------------------
USEFORM("Unit1.cpp", Form1);
//---------------------------------------------------------------------------
WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{

なぜ、_tWinMain ?

あ、_tWinMainに戻り値型の宣言がない。気をつけましょう。

Anonymous