VC6 と VC7 の互換性

SPECIAL


Visual C++ の互換性…

Visual Studio .NET を買って早数日…。

COM コンポーネントのバージョンアップを図るべく Visual C++ 7.0 にて ATL COM のプロジェクトをコンパイルしなおすことになりました。

 

それまでに静的ライブラリを Visual C++ 7.0 にてサクッとコンパイルすることができていたので、COM コンポーネントも何気なくコンパイルできて当然のものと思っていました。

が、なにやらいっぱいエラーが…。

 

エラーの大半が、関数系の多重定義…、といいますか、実装が重複し待っているようです。

幸い、コンパイラのバージョンアップに伴う障害である可能性が極めて高いので、とりあえず、Visual Studio .NET に付属していたオンラインドキュメントを調べてみることにしました。

 

MFC 7.0 と ATL 7.0

コンポーネントがらみであったので、とりあえず ATL 関連の文書をあたってみたところ、どうやら Visual C++ 7.0 では ATL まわりと MFC まわりで大きな変更があったとのことです。

今回いじろうとした COM コンポーネントは、ATL COM の他、MFC の静的ライブラリを組み込んでいたということで、どうやらこのあたりをいっぺんに体験してしまったようです。

 

 

変更点 ATL MFC
DLL の非互換性 該当 該当
ATL モジュールクラス 該当  
文字列の変換 該当  
BSTR から CString への変換 該当 該当
CComEnumImpl::Skip の変更点 該当  
CWnd::DestroyWindow のアサーション ( assertion = 主張? )   該当
LNK2001 未解決の外部シンボルエラー   該当

とりあえず、掲げられていた重要な変更点は上の項目のようです。たくさん変わっていますね…。心当たりあるものばかりです。

 

変更点の補足

DLL の非互換性

MFC の DLL は mfc70.dll に、そして ATL の DLL は atl70.dll になったようです。

これらは同じクラスであっても、たとえば mfc42.dll で実装されていたものとはバイナリレベルでの互換性がなく、Visual Studio .NET にて改めてビルドしなおす必要があるそうです。

 

ATL モジュールクラス

以前の ATL の CComModule が、ATL 7.0 では次のクラスに分けられたそうです。

CAtlBaseModule HINSTANCE とリソースインスタンスを含む、ほとんどの ATL アプリケーションに必要
CAtlComModule COM クラスに必要
CAtlWinModule ウィンドウクラスに必要
CAtlDebugInterfacesModule インターフェイスデバッグのサポート
CAtlModule 特定のアプリケーションに必要な基底クラス
CAtlDllModuleT : CAtlModule DLL 用の標準エクスポートを提供
CAtlExeModuleT : CAtlModule 実行ファイル用のコードを提供
CAtlServiceModuleT : CAtlModule サービスプログラムの作成をサポート

 

これによって、機能の細分化や、従来は必要だったらしい Init メソッドと Term メソッドをコンストラクタとデストラクタに移動するなどが実現したそうです。

また、従来の CComModule は、互換性を保つため、現在も使用することができるようです。

 

文字列の変換

ATL 3.0 (Visual C++ 6.0) までの、システムの ANSI コードページ (CP_ACP) を用いた文字列変換から、実行中のスレッドの既定の ANSI コードページによる変換に変わったそうです。

_CONVERSION_DONT_USE_THREAD_LOCALE を定義すると、以前と同様のシステムの ANSI コードページによる変換が可能のようです。

 

文字列の変換マクロは次のような命名規則で用意されています。

CSourceType2[C]DestinationType[EX]

SourceType から DestinationType への変換マクロで、[C] と [EX] はそれぞれ、C と EX を省くことが出来るという意味です。

[C] の部分は、この C を入れることで変換後の値を定数とすることができ、[EX] の部分は、この EX を入れることでバッファの初期サイズを指定することが出来るということのようです。

SourceType と DestinationType のところに指定できる文字は次の通りです。

 

A ANSI 文字列です。
W UNICODE 文字列です。
T 標準文字列です。_UNICODE が定義されていれば W を、それ以外は A を意味します。
OLE OLE 文字列です。W と同等です。

 

このマクロによって、以前と比べて次の点が改善されたそうです。

  • 小さい文字にはスタックメモリを、スタックが不足する場合はヒープをしようする。
  • 関数を終了する前でも、スコープを抜けた時点で解放される。
  • ループごとに解放されるようになったため、ループ内でも通常通り使用可能。
  • 大きい文字列の取り扱いにも適している。
  • USES_CONVERSION を定義する必要がなくなった。
  • OLE の意味が W で固定になった。

 

BSTR から CString への変換

何の断りもなく、CString 型の変数へ BSTR 型の値を格納しようとするとエラーが発生するようになったそうです。

その代わり、CString 型は ANSI 用の CStringA と UNICODE 用の CStringW とが用意されたようで、CStringW 型の変数にならば直接、BSTR 型の値を格納することが出来るようです。

 

ただ CString 系クラスは、ANSI と UNICODE 間の変換機能も備わっていて、それを利用したい場合には、明示的にコンストラクタを呼ぶことで解決できるようです。

 

CString str = CString(bstr);

たとえば、CString 型へ BSTR 型を渡したい場合には、上のように BSTR 型を CString クラスのコンストラクタへ渡すことで、変換可能です。

 

CComEnumImpl::Skip の変更点

これは変更点というより、バグ修正と捕らえた方がいいようです。

CComEnumImpl というのはコレクションを実装しやすくするクラスだそうですが、従来の ATL 3.0 はこの Skip メソッドに 0 を渡した場合に正しいエラーコードが得られなかったり、大きい値を渡した場合の処理に一貫性がなかったりしたそうです。

 

CWnd::DestroyWindow のアサーション

これもバグフィックスでしょうか…。

とりあえず、従来の CWnd::DestroyWindow でツールヒントを表示するとアサーションエラーが発生していたそうです。

ので、次に示すメンバ変数を AFX_THREAD_STATE から AFX_MODULE_THREAD_STATE に移動されたそうです。

  • CToolTipCtrl* m_pToolTip
  • CWnd* m_pLastHit
  • int m_nLastHit
  • TOOLINFO m_lastInfo
  • int m_nLastStatus
  • CControlBar* m_pLastStatus

 

明記されていませんでしたけど、きっとこれでアサーションエラーが表示されなくなっているのでしょう。

これがどのような対処だったのか良くわかりませんけど、たぶん、メモリが解放されるタイミングを変えたとかそういう対処なのでしょうか…。

 

LNK2001 未解決の外部シンボルエラー

静的ライブラリや DLL で、wchar_t 型をとる関数を呼び出すときに LNK2001 未解決の外部シンボルエラーが発生する場合があるそうです。

なお、BSTR や LPWSTR という型も wchar_t* に解決されるそうで、これもこのトラブルの要因となるようです。

 

これは、従来は unsigned short として処理されていた wchar_t が、/Zc:wchar_t というコンパイラオプションによって、wchar_t 自体がネイティブな型として処理されるために起こるそうです。

新規の MFC プロジェクトではこのオプションが既定で ON となるそうで、このオプションが ON でなかったころに作成したライブラリと、ON のプロジェクトとが共存した場合、関数シグネチュアが一致しなくなるそうです。

 

エラーを取り除く…

さて一通り、互換性の問題を調べてみましたので、あとはその問題を解消するだけ…、という感じですね。

 

DLL 周りのチェック

とりあえず、MFC とか ATL とかの部分をチェックしてみました。

プロジェクトのプロパティの 「全般」 を見てみると、なぜか ATL COM のはずが、Unicode Debug 構成では ATL を使用しない設定になっていたので、ATL を静的リンクで使用するようにしました。

そういえば、MFC は明示的に静的リンクした記憶はあるけれど、ATL はそんな問いを見た記憶がないので、もしかすると新たに DLL 化したのかも…。

とりあえずこれでリビルドをかけてみましたが、特に改善は見られませんでした。

 

LNK2001 未解決の外部シンボルエラー

とりあえず Visual C++ 7.0 で標準なら、wchar_t をビルドインにしようかと思ったのですけど、先日あらたに Visual C++ 7.0 で作成した静的ライブラリは wchar_t のビルドインにはなっていなかったので、とりあえずこの COM も wchar_t のビルドイン定義はしないことにしました。

 

LNK2005 既に libcmtd.lib で定義されています

とりあえずいちばん目立つ警告メッセージがこの libcmtd.lib で定義されているものが幾重にわたって出てきてしまうようです。

また、最後の方で ”defaultlib 'LIBC' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。” という警告も出てきています。

 

ここで関わってきている libcmtd.lib というのは、コンパイラオプションで /MTd を指定し、マルチスレッド対応のランタイム ( "d" が付くのでそのうちのデバッグバージョン) を使用する際に使われるライブラリのようです。

 

デバッグビルドとリリースビルドとの共存

デバッグバージョンとリリースバージョンのランタイムを共存しないようにした方がいいとの記載もあったので、もしかするとこれかもしれません。

というのも、Visual C++ 7.0 にした際に、スタティックライブラリを組み込んだ COM になったためです。

 

ライブラリはリリースビルド済み。そして今つくっている COM はデバッグモード…。

ということで、とりあえず早速、COM の方を Unicode Release MinDependency でビルドしなおしてみると、libcmtd.lib に関するリンクエラーはすっかりなくなりました。

 

ならばと、静的ライブラリのデバッグ版を別のファイル名にて作成し、COM の Unicode Debug にリンクさせてみたところ…、相変わらずエラーです。

これは互換性とかの問題ではなく、単純にリンカとか静的ライブラリの知識不足のようですね…。

 

Debug 番で、標準 C++ ライブラリを、シングルスレッドから、COM で指定しているマルチスレッドに変更したところ、見事、多重定義のエラーはなくなりました。

…、ということは、4種類作らなくてはいけないのでしょうか…。

いきなり動かすものでもないですし、ランタイムライブラリを排除できればいいのですけどね。

試してみたところ、どうやら /MT 系のマルチスレッド対応の静的ライブラリを作れば、シングルスレッドを示す /ML の方でも何とか平気のようです…。

また、/MTd の COM で /MT の静的ライブラリを読み込んでみたところ、

LINK : warning LNK4098: defaultlib 'LIBCMT' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。

という警告は出るものの、致命的なエラーにいたることはないようです。

またこの警告は、【リンカ】 の 【入力】 で、libcmt.lib を除外することで回避することも出来るようなので、とりあえずはそのままでよさそうです…。

 

とりあえずこの段階で、警告こそあるものの、ビルドに失敗するというほどではなくなりました。

 

CRegKey::QueryValue …

残っている警告のうち、ひとつは CRegKey クラスまわりの様子です。

何でも、”ATL::CRegKey::QueryValue が古い形式として宣言されました” という警告メッセージが表示されています。

 

…、CRegKey って MFC だったような…。

Visual Studio .NET のドキュメントで調べてみると、どうやら CRegKey は ATL に所属しているようです。…、と思えば、MSDN 2001/04 では ATL と MFC の両方にあるような感じです。

 

とりあえず ATL だしということで、ATL のドキュメントを見てみると、QueryValue メソッドは互換性のために残されているようです。

この QueryValue はオーバーロードを使って、引数に応じてレジストリの型を指定する使用になっていましたが、新しい CRegKey では QueryStringValue や QueryDWORDValue といったように、メソッド名を使って区別するように変更になったようです。

実際、この方がソースも見やすくなるし、変なところで頭を使わなくていいのでいい感じです。

 

ATL モジュールクラス

そして若干気になるのが、次のメッセージ。

statreg.cpp is obsolete. Please remove it from your project.

atlimpl.cpp is obsolete. Please remove it from your project.

statreg.cpp と atlimpl.cpp が不要とのことですけど、おそらくこれは CComModule が細分化されたのに関するメッセージではないかと思います。

ただ、CComModule を差し替えるにもいったいどのような手順を追ったらいいか良くわからず…。とりあえずはビルドできるので良しとしましょうか…。

 

と思いつつも、コンパイル時のメッセージを眺めてみると、StdAfx.cpp にて、この警告が表示されているのがわかりました。

ということで StdAfx.cpp を開いてみると、しっかりとこの2つのファイルがインクルードされていたので、コメントアウトしてみました。

すると、この警告メッセージは表示されなくなりました。

 

DLL エントリ

あと、残すところのエラーは、DLL の EXPORTS にて、序数を割り当てないでくれとの警告メッセージです。

 

  • DllCanUnloadNow
  • DllGetClassObject
  • DllGetClassFactoryFromClassString
  • DllInstall
  • DllRegisterServer
  • DllRegisterServerEx
  • DllUnregisterServer

ドキュメントで LNK4222 を調べてみると、上のエントリを序数でエクスポートしないでくれとのことです。

ということで、念のためコメントアウトしつつも、これらについている序数 @1 とかを消してみることにしました。

 

USES_CONVERSION

あとは、不要になったらしい USES_CONVERSION を探してみます。

そしてこれを削除してみると、しっかりとエラーが出てしまいました。これは新しい ATL COM プロジェクトを立ち上げないとだめなのでしょうか…。

 

これは、従来の W2A マクロとかでは依然として USES_CONVERSION が必要で、USES_CONVERSION を取り除いた場合は、CW2A などに置き換える必要があるようでした。

 

この CW2A ですが、次のように書くとバグの原因となるので気をつけましょう。

char *temp = CW2A(bstrString);

CW2A は実質的にクラスで構成されているようで、CW2A で変換された文字列は、そのクラスの内部バッファへ記録されるようです。

上記のようなプログラムを実行した場合、CW2A にて内部バッファのポインタを temp に取得できるまではいいのですが、その後、役目を終えた CW2A のクラスが破棄され、内部バッファが解放されてしまいます。

 

COM 動かず…

さて、エラーも取り除けたということで動作させてみたのですけど、CreateObject をして、そのあとでメソッドを呼び出してみると…、DLL の読み込みでエラーが発生してしまいました。

どうやらうまく移行できなかったようです。

 

ちょっと関係ないのですけど、調べていると 「ATL で CRT をできるだけ使用しない」 という欄にチェックを入れると、_ATL_MIN_CRT という定義が追加されるようです。

以前にこのような警告を見たことがあったので念のためここ (↑) へメモしてみました。