syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

BSTRはwchar_tへのポインタではありません

COMの文字列にはBSTRというデータ型が使われています。
BSTRが定義されているWindows SDKのヘッダーを見てみましょう。

// <wtypes.h> in Windows SDK 8.1

#ifndef _PREFAST_
typedef /* [wire_marshal] */ OLECHAR *BSTR;
#else // _PREFAST_
typedef _Null_terminated_ OLECHAR *BSTR;
#endif

OLECHARはもともとCOMの前身であるOLE (Object Linking and Embedding) で使われていた文字型の名残で、当時はANSI MBCSのcharでしたが、現在はWCHARすなわちwchar_tと等価です。Windows上ではsizeof(wchar_t) == 2であり、UTF-16エンコーディングUnicode文字を表現するのに使われます。

// <WTypesbase.h> in Windows SDK 8.1

#if defined(_WIN32) && !defined(OLE2ANSI)
typedef WCHAR OLECHAR;
typedef /* [string] */  __RPC_string OLECHAR *LPOLESTR;
typedef /* [string] */  __RPC_string const OLECHAR *LPCOLESTR;
#define OLESTR(str) L##str
#else
typedef char      OLECHAR;
typedef LPSTR     LPOLESTR;
typedef LPCSTR    LPCOLESTR;
#define OLESTR(str) str
#endif

「ふーん、じゃあBSTRwchar_tへのポインタなんだね。てことは、BSTRを要求するAPIにはwchar_tへのポインタを渡してもいいんだ!」

ダメです。絶対にやめてください。

そもそもBSTRとは?

BSTRはCOMだけでなく旧VB/VBAでも使われている、長さ情報を持った自己記述的文字列型の一種です。Windows SDKの <oaidl.h> で定義されているVARIANT型とも深い関わりがあります。
BSTRのデータ構造に関しては、以下の資料をよく読めばイメージできるはずです。

実際にコードで試してみます。

#define NOMINMAX
#include <Windows.h>
#include <tchar.h>
#include <cstdio>
#include <clocale>
#include <initializer_list>

int main() {
    _tsetlocale(LC_ALL, _T(""));

    for (auto str : { L"abc", L"hoge", L"Hello" }) {
        // BSTR の型自体は OLECHAR* すなわち wchar_t* の typedef エイリアスだが、
        // 実際の BSTR データ構造は先頭に4バイトの長さ情報(符号無し整数)を持っている。
        // Pascal の自己記述型文字列と、C の NUL 終端文字列の両方をミックスしたような構造。

        // SysAllocString() は、終端 NUL 文字の分を加味した (4 + UTF-16 文字列バイト長 + 2) の長さを持つバイト配列を確保し、
        // 5バイト目以降へのポインタを BSTR 型として返却しているにすぎない。
        // SysFreeString() は、渡されたポインタからマイナス方向に4バイトオフセットした位置を先頭とするバイト配列を解放する。
        // SysStringByteLen() や SysStringLen() は、渡されたポインタからマイナス方向に4バイトオフセットした位置に長さ情報が格納されていると仮定して値を読み取っている。
        // これにより、SysStringByteLen() と SysStringLen() の計算量オーダーは O(1) となる。
        // したがって、BSTR を要求する API に wchar_t* を渡してはいけない。
        // const wchar_t* や const wchar_t[] を BSTR にキャストするのもダメ。
        BSTR bstr = ::SysAllocString(str);
        const UINT sizeInBytes = *(reinterpret_cast<UINT32*>(bstr) - 1);
        const UINT sysByteLen = ::SysStringByteLen(bstr);
        const UINT sysStrLen = ::SysStringLen(bstr);
        wprintf(L"BSTR: wsz = \"%ls\", size = %u[bytes], byte-len = %u, len = %u\n", bstr, sizeInBytes, sysByteLen, sysStrLen);
        ::SysFreeString(bstr);
        bstr = nullptr;
    }
    // BSTR を偽装してみる。通常はマネしないように。
    // 本来の BSTR データ構造であれば、リトルエンディアンのバイト長として (2 * 3) == 6 が格納されていなければならない。
    // また、バイト長は2の倍数すなわち偶数でなければならない。
    struct { INT32 len; wchar_t str[128]; } dummyData = { 65535, L"abc" };
    BSTR bstrIllegal = dummyData.str;
    const UINT sysByteLen = ::SysStringByteLen(bstrIllegal);
    const UINT sysStrLen = ::SysStringLen(bstrIllegal);
    wprintf(L"Illegal BSTR: byte-len = %u, len = %u\n", sysByteLen, sysStrLen);
}

以上のように、BSTRはメモリの確保/解放や長さ取得に専用のAPIを利用します。
BSTRをC/C++で読み取る際はwchar_tのゼロ終端文字列として扱うこともできますが、その逆は不可です。決してwchar_tのゼロ終端文字列をBSTRとして扱ってはいけません。未定義の異常動作を引き起こす原因になります。

BSTRのラッパー

Visual C++ではATL::CComBSTR_bstr_tといったラッパークラス型(スマートポインタ)が用意されており、ライフサイクル管理の煩雑なBSTRを比較的簡単に扱うことができます。
WIL (Windows Implementation Library) にもwil::unique_bstrが用意されています。

ただし、BSTRラッパーの内部では結局システムAPIをコールするので、ヒープ管理にCRT (C Runtime) を利用するATL::CStringWや標準C++std::wstringと違い、速度やメモリ効率などのパフォーマンス面で不利です。
BSTRラッパーはBSTRを要求するCOMコンポーネントとの境界面 (API) にのみ使用し、普段は文字列の管理にATL::CStringWstd::wstringを使うべきです。

WindowsのOutputDebugString()関数とUnicode

WindowsOutputDebugStringW()関数とOutputDebugStringA()関数は、デバッグコンソールに文字列を出力します。

Visual Studioデバッグ実行すると、「出力ウィンドウ」に文字列が表示されます。
ただしAndroidのLogcatや、MacXcodeNSLogと違って、末尾に改行は自動付与されません。
DebugViewというツールを使うと、関数呼び出しごとの出力レコードをリスト表示することができます。Visual Studioデバッグ実行は動作が重たくなりがちですが、DebugViewはかなり軽快です(UIが古臭いのが欠点)。また、OutputDebugString()はリリースビルドでも使えるので、Visual Studioをインストールせずにデプロイ環境で手軽にログ出力しながら動作確認したいときに重宝します*1

ここで、

OutputDebugStringW converts the specified string based on the current system locale information and passes it to OutputDebugStringA to be displayed. As a result, some Unicode characters may not be displayed correctly.

と説明されているんですが、Visual Studio 2013までは確かに以下のようなコードは文字化け*2して日本語cR???となってしまうものの、Visual Studio 2015以降は正しく表示できています。

#include <Windows.h>

int main() {
    ::OutputDebugStringW(L"日本語©®™♡♥\n");
}

以下の説明から推察するに、VS2015ではWindows 10で追加されたAPIWaitForDebugEventEx()」をコールすることで、Unicode出力を実現しているようです。確かWindows 7/8.x上では、VS2015であってもOutputDebugStringW()Unicode固有文字を出力することはできていなかったように思います。

Important
In the past, the operating system did not output Unicode strings via OutputDebugStringW and instead only output ASCII strings. To force OutputDebugStringW to correctly output Unicode strings, debuggers are required to call WaitForDebugEventEx to opt into the new behavior. On calling WaitForDebugEventEx, the operating system will know that the debugger supports Unicode and is specifically opting into receiving Unicode strings.

ATLTRACE

ATL/MFCアプリやWinRTアプリでは、デバッグビルド時のみ有効になるATLTRACE()マクロ (atltrace.h) を使ってOutputDebugString()を間接的に利用することが多いですね。このマクロは第1引数に書式文字列を受け取るので便利です。MFCTRACE()マクロ (afx.h) は、ATLTRACE()と同じです*3

ただしVS2013 (ATL 12.0) 以降のATLTRACE()では、

hoge.cpp(123) : atlTraceGeneral - 出力メッセージ

というような感じで、ファイルパスと行番号と診断レベルが出力メッセージ先頭に付加されるようになりました。
Visual Studio IDEの出力ウィンドウでは、「FilePath(LineNumber):」の形で表示されたメッセージをダブルクリックすると、該当するソース行にジャンプできる機能が昔からあり、そのため __FILE____LINE__ を使って書式化されたメッセージを出力することがよくあったんですが、特にユーザーコードで工夫するまでもなく自動的にこの機能が利用できる形になりました。
ただ、常時出力されてしまうため、情報量が増えることでノイズも増え、メッセージ本体が埋もれてしまってうっとうしいと感じることもあります。

System.Diagnostics.Debug

.NETのDebug.WriteLine()メソッドやDebug.Print()メソッドは、VS2012/VS2013でもUnicode固有文字を出力できました。

System.Diagnostics.Debug.WriteLine("日本語©®™♡♥");

System.Diagnostics.Debugも内部的にOutputDebugStringW()を使っていると思っていたんですが、以下の解説によると、マネージコードに関しては別のデバッグエンジンが使われるため、OutputDebugStringW()の問題を回避できるようです。

Note
Visual Studio includes its own debugging environment and debugging engine, which together are called the Visual Studio debugger. For information on debugging in Visual Studio, see Debugging in Visual Studio. For debugging managed code, such as C#, using the Visual Studio debugger is often the easiest way to get started.

記憶が定かではないのですが、.NETのほうは確かWindows 10だけでなく、Windows 7/8.xでもUnicode固有文字を出力できていたような気がします。

*1:逆に言うとエンドユーザーでもこのDebugViewを使えばデバッグログをキャプチャすることができるので、リリースビルドで重要な情報をログ出力するのはご法度です。普通はデバッグビルドでのみ出力されるようにします。

*2:厳密に言うと「文字化け」というよりは、Unicode文字からCP932 (Shift_JIS) 文字へのマッピングが不可能であるために、代替文字に自動変換されています。

*3:昔はATLとMFCは別々に実装・メンテナンスされていたのですが、VC7で統合・整理されました。以後、MFCアプリからはATLの機能がすべて使えるようになっています。さらにVC2013以降はDLL版のATLが廃止され、スタティックリンク版のみになりました。Microsoft C/C++ 2003 - 2015 の変更履歴 | Microsoft Learn

絶対値関数absとオーバーフロー

C/C++において、符号付き整数の絶対値を求める関数abs(), labs(), llabs()および整数型のstd::abs()関数オーバーロードには罠があり、「整数オーバーフロー」が考慮されていません。
これは、JavaC#のような近代的な言語と違って、C/C++の整数内部表現が2の補数系であるとは限らない、ということも関係しています。

C言語では、2の補数系において、整数型を受け取り整数型を返す絶対値関数にINT_MINLONG_MINなどが渡されたときには、各絶対値がそれぞれの型intlongなどで表現不可能なため、未定義動作 (undefined behavior) を引き起こします。要するにそれらの値を渡した場合は実際に何が起きるか分からないので絶対に渡すな、事前にチェックしろということです。

同様にC++では、2の補数系において、整数型を受け取り整数型を返すstd::abs()関数オーバーロードINT_MINLONG_MINなどが渡されたときには、未定義動作 (undefined behavior) を引き起こします。
なお、C++03以前では、<cmath>には浮動小数点数型を受け取るstd::abs()オーバーロードのみが存在しますが、C++11以降では整数型を受け取るオーバーロードも存在します。
また、C++14以前では、<cstdlib>には整数型を受け取るstd::abs()オーバーロードのみが存在しますが、C++17以降では浮動小数点数型を受け取るオーバーロードも存在します。

C# (.NET) のMath.Abs()メソッドではOverflowException例外がスローされます。

JavaMath.abs()メソッドでは入力と同じ値を返します。

C/C++では、関数呼び出し側が入力の範囲を保証するべき(オーバーヘッドを最小化するために関数内部での入力チェックを省く)、という設計思想の関数が多いんですが、いちいちアプリケーションコードで入力チェックするラッパーを書くのもダルいので、C/C++でもsafe_abs()みたいな関数を標準化して欲しいですね。
成否をbool型またはerrno_t型の戻り値で返して、成功すれば絶対値を引数T& or T*で返すとか。あるいはC++17で追加されたstd::optionalを使って戻り値として返す設計でも良いと思います。
整数オーバーフローが発生すると、以後の計算結果が不正になる可能性が高いので、いっそのこと.NETのように例外を投げてしまい、強制的に処理を中断するというのもアリだと思います。以下ではJavaでラッパーメソッドsafeAbs()を実装して例外処理する手法が紹介されています。

ちなみにC++20からは、ついに符号付き整数型が2の補数表現であることが規定されました。現実がまったく見えていない、旧世紀に生きているC++標準化委員会の干からびた老人どもがようやく重い腰を上げた、といったところですが、JavaC#に対して実に20年以上遅れていましたね。

Cの最新規格C17 (C18) では今のところ規定されていませんが、次期標準規格 C2x では2の補数表現のみが許容されるようになることが N2412 によって提案されているそうです。

今後、C言語でも2の補数表現が規定されるようになったら、少なくともJavaのように、絶対値関数が返す値を規定して未定義動作を排除するか、あるいは返す値を規定した標準ラッパーを定義して欲しいところです。

余談ですが、C/C++Unicodeサポートもかなり遅れています。JavaC#はともに20年以上前の登場当初からUTF-16を標準サポートしていたのに、C/C++では未だにchar16_t文字列を標準出力することすらままなりません。時代遅れとかいうレベルを超越しています。現実が何ひとつ見えていない。それだけでなく、UTF-8リテラルの型も結局charから後出しのchar8_tに変更されるなど、最近は仕様策定ミスを撤回するために互換性を破壊する仕様変更が頻繁に入るようになりました。ちょっと考えれば分かりそうなことなのに、標準化委員会は後先考えることもできない無能集団以外の何物でもありません。自称・C++のエキスパートらしいですが、現役のプログラマーやエンジニアですらない頭でっかちなダボハゼ連中です。

※2023-02-19追記:
C++23でようやくUnicode文字列の標準出力をサポートするstd::vprint_unicode()が追加されるようです。すさまじい周回遅れ。

C/C++よりも安全なシステムプログラミング言語としてRustが台頭してきたので、C/C++の標準化委員会は焦っているんでしょうね。しかしC/C++には依然として未定義動作・未規定動作や処理系依存動作が多数残っており、安全性の問題は解決されていないので、このままだと確実にC/C++は時代遅れの化石となることが目に見えています。

日本語のIT関係記事は提灯であふれている

日本(日本語)のIT関係記事は、意図的に事実を捻じ曲げ、読者に誤解をさせるような偏向的な眉唾記事であふれています。すべてステークホルダーへの忖度です。

個別のテクノロジーに精通している人間でなくとも、海外ベンダーが英語で公式に発信している原文(一次情報)に触れれば、ライター連中が日本語で書いていることが嘘八百であることにすぐ気づくのですが、日本人は英語ができないため、英語の一次情報にほとんど触れようとしない*1*2ことから、簡単にだませます。

pcnomori.blog.fc2.com

最近見た記事では、Apple M1に関する笠原一輝の記事(↓)が一番ひどかったですね。低俗な日本のメディアによくある、典型的な釣りタイトルです。連中にとっては情報の正確性よりもPV数のほうが大事なんでしょう。
特にコイツのベンチマークGPUとグラフィックスAPIを知っている人間からすれば噴飯ものです。Windows+OpenGLMac+Metalで比較するとか、意図的にMac側に有利な条件でテストしておいて、このタイトルですからね。そもそも中学生でも知っているような対照実験の原則を完全に無視しています。
Appleから金をつかまされてクソみたいな提灯記事を書いているのか、それともこの笠原一輝がただのApple信者なのかは知りませんが、こんなひどいバイアス記事を書いて金をかせいでいるクズが日本にいて、さらにそれを盲目的に信じる情報弱者が少なからずいるのかと思うと情けなくなります。

pc.watch.impress.co.jp

*1:実際はライティングはともかくリーディングに関しては高校卒業レベルでも大抵なんとかなるはずで、読めるはずなのに読もうとしないだけです。要するにただの英語アレルギーですね。

*2:Wikipediaではなぜか「二次情報源 > 一次情報源」というおかしな主張がまかり通っていますが、二次情報が不確かで間違っているということはよくあります。特に日本語への翻訳は誤訳だらけなのに、母語で書かれた二次情報のほうが正確であると盲目的に信奉する人間が多いようです。

横浜・林、帯状疱疹で退場

なんだこの無能。ロクに仕事をしないどころか、このタイミングで貴重な医療リソースを無駄遣いするなど、言語道断。
早く消えてくださいこのカジノキチガイ脳。

なんでこんなゴミクズみたいな程度の低いクソザコ老害が市長になれるかっていうと、横浜市民の大半が同様に利権まみれのゴミクズだからです。
自民とか公明とかの看板が近所の民家のあちこちに立っていて本当に気持ち悪い。

あんたら……「自覚して生きている人たち」……ですよね?
こんなクソ政党どもの看板を掲げるっていうことは、

私、無知無能の奴隷です\(^q^)/ アゥアゥ
私のおうちをつぶして原発作ってください\(^q^)/ アゥアゥ
増税バンザーイ\(^q^)/ アゥアゥアゥッ

と言ってるようなもんだっていうことを「自覚して生きている人たち」ですよね?

道案内は冷静に対処しよう

道路を歩いていると、ときおり誰かから道を聞かれることがあります。駅で電車の乗り場や方向・停車駅を聞かれることもあります。話しかけやすいオーラでも出ているんでしょうか。

うまく道案内できたときもあるんですが、間違って反対側を教えてしまったことが何度かあります。ずいぶん昔のことであっても、そういう失敗はいまだに覚えています。つい先日もやらかしてしまいました。罪悪感と自己嫌悪に陥っています。教会で懺悔したい気分です。近所の寺でもいいんだろうか。

後悔先に立たず、ここはまず文書化することで要点を整理できるはず。
二度と同じ間違いを繰り返さないよう、懺悔も兼ねてケーススタディとして明文化しておきたいと思います。

失敗事例1:実家

実家は広島県のド田舎にあるんですが、中学で転居・高校で親元を離れたこともあって、実家でありながらあまり地理には詳しくありません。
学生の頃、確か20年くらい前のことでした。近所を散歩していたとき、乗用車を運転してきた男性から分かれ道で方向を尋ねられたんですが、誤って(よく似た地名の)反対方向の道を教えてしまったことがあります。数秒後に間違いに気づいたときにはすでに遅し。
当時はGoogle Mapはおろかカーナビもほとんど普及していない時代だったし、人もまばらな過疎地なので、あの後おそらく道に迷って苦労されたんではないかと思うと、未だに罪悪感をおぼえます。本当に申し訳ない。

教訓1

どうすれば間違いを防ぐことができたのか、よく分かりません。クルマに同乗して案内することはさすがにできないものの、せめて紙に文字で書いてくれていれば間違わなかったかもしれません。
案内する側の教訓としては、よく似た地名を混同しないように冷静にヒアリングすること、でしょうか。
今はほとんどのクルマにカーナビが装備されているので迷うことはないと思うのですが、うちの実家は相変わらず携帯電話の電波がほとんど届かない土地です。
案内される側の教訓として、不慣れな土地をドライブされる方は、自分のように地元の人間なのに間違った道案内をしてしまうアホがいること、またド田舎ではインターネットなぞ頼りにならないので、GPSベースのカーナビは必須(Google Mapのようなインターネットベースのショボいナビは絶対ダメ)ということは肝に銘じておかれたほうがよいかと。

失敗事例2:街中

つい先日ですが、神奈川県内の最寄の駅で年配の方からタクシー乗り場を聞かれました。駅には複数のタクシー乗り場があり、とっさによく知っている乗り場のほうを答えたのですが、人通りや停車数が少ないほうを間違って教えてしまいました。
10年も住んでいるのに何をやっているのか……本当にアホな案内をしました。すぐあとで気づいたんですが、人通りが多いほうにはタクシーが数台待っていて、そちらを案内しておけば即座に乗れたであろうに……

教訓2

間違いに気づいたら早めに訂正しましょう。歩くのが遅い年配の方であれば間に合う可能性が高いです。罪悪感で寝覚めが悪くなり、精神的に不安定になることに比べたら、10分や20分程度のロスなど大したことではありません。
また、方向を教えるだけではなく、近くであればその場所まで同行するとなお良しです。ただし女性が男性についていくのは危険も伴うので難しいところです。もし駅や交番が近くにあれば、そこまで同行して、あとは駅員や警察官の方々に任せるのがベストです。信頼できる職務の方々に任せれば、あとは罪悪感に陥る必要はありません。

成功事例1:大船駅

5年ほど前の話です。大船駅の西口付近で道を聞かれたことがありました。行き先を聞いてネットで調べたところ、湘南モノレールの駅だろう、ということが判明。おそらく中国系の人で、日本語は多少話せる程度、英語も通じるようでしたが、口頭で案内するには難しかったため、モノレール乗り場まで同行しました。
転職前の勤務先は大船駅近くで、当時会社に向かう途中でしたが、そもそも盛大に遅刻していたので、どうせ遅れるのであれば人助けしていこう、という気持ちでいたことが良かったのかもしれません。

総論

自分自身が不慣れな土地で突然聞かれたのであれば道案内に失敗するのも仕方ないこともありますが、地元や近所で道案内に失敗するのは痛恨の極み以外の何物でもありません。
よく知った土地であっても、記憶が曖昧であれば、いったんネットで検索して情報を補強します。冷静さを取り戻すきっかけにもなります。

ボーっと生きていると、毎日通る道にあるものにすら気づきません。もっと心に余裕を持ち、観察力を鍛えましょう。

KDDIは潰れるべきクソ企業

DMM通販の配送待ちで待機していたのでついついドアを開けてしまったんですが、インターホン越しに映像確認しておくべきだったと後悔しました。

こういう気持ち悪い訪問営業が来て回線のことを遠回しに根掘り葉掘り聞こうとしてきます。答える義務は一切ないので追い返しましょう。
CMも気持ち悪ければ営業も気持ち悪い。吐き気がするクソ企業。

togetter.com

セキュリティ意識ゼロのdocomoも死ねばいい企業の筆頭だけど、KDDIは真っ先に潰れるべき。
ソフトバンク光も電話営業がしつこいんですが、発信元の電話番号からネットで素性を調べて着信拒否すればどうってことありません。
ていうかこの3大キャリアどもはつくづく顧客をバカにしてますね。死ねばいいのに。