もくじ
https://tera1707.com/entry/2022/02/06/144447
やりたいこと
以前の記事で、COMをC++とC#から使う練習をしたつもりだったが、再度COMを使うことになったときに、全然わかってなかったことに気づいてしまったので、もうちょっと頑張ってCOMを使う練習をしたい。
※既存のCOMのインターフェースを使う(COMクライアント)のを練習する。COMを作る(COMサーバー)側はまた今度、、、、
前提
勉強しながらのメモ書きなので、誤りあるかも。
「~~という理解。」とか書いてるところは、特に自信ないところ。
書きたい内容
- CoClassの取得の仕方
- newするパターン
- Activator.CreateInstance(CLSID.SensorManagerType) するパターン
- インターフェースのメソッドの宣言の仕方
- 前から順番に書いてく方法
- 使わないヤツは適当に書く場合
- _VtblGap1_17 などと書く場合 -メソッドの宣言の仕方
- PreserveSig =trueにして、そのままのシグネチャにする方法
- PreserveSig =falseにして、引数のoutを戻り値に持ってくる方法
- その他
, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
は必ずつけましょう- なくてもIFの取得はできたが、outがintとかの場合(GetCountとか)に、うまく値が取れなかった
今回やること
以前の記事でやったセンサーを取るCOMをもう一度やる。
今回、照度センサがついたPCが手元にあるので、それを使って、COMで、C++とC#から、センサーの値を取れることと、その手順をはっきりさせるのを目標にする。
コード置き場所
https://github.com/tera1707/COM_JikkenWithSensor
C++でやってみる
まずは、C++で、COMを使ってやりたいことの実現をする。
最初からC#でCOMを使おうとするのは、今の私には厳しそう。
書いたコード全部
#include <stdio.h> #include <InitGuid.h> #include <SensorsApi.h> #include <Sensors.h> #include <string> #pragma comment(lib, "Sensorsapi.lib") // https://learn.microsoft.com/ja-jp/windows/win32/sensorsapi/sensor-category-light int main() { ISensorManager* pSensorManager; ISensorCollection* pMotionSensorCollection; ISensor* pAmbientSensor; CoInitialize(NULL); auto hr = CoCreateInstance(CLSID_SensorManager, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSensorManager)); pSensorManager->GetSensorsByCategory(SENSOR_TYPE_AMBIENT_LIGHT, &pMotionSensorCollection); ULONG count = 0; pMotionSensorCollection->GetCount(&count); for (int i = 0; i < count; i++) { pMotionSensorCollection->GetAt(i, &pAmbientSensor); ISensorDataReport* pData; pAmbientSensor->GetData(&pData); // 照度センサの値(ルクス)を取る PROPVARIANT x = {}; pData->GetSensorValue(SENSOR_DATA_TYPE_LIGHT_LEVEL_LUX, &x); wprintf(std::to_wstring(x.fltVal).c_str()); } CoUninitialize(); }
やったこと
まずはC++で書いた。処理の流れを挙げる。
COMを初期化する
CoInitialize(NULL);
※引数はNULL固定でOK。
これは、「COMを使えるようにするために、現在のスレッドをアパートメントに入れる」ということをしているらしい。(まだ少々理解不足...)
試しに、CoInitialize(NULL)
した後に、別スレッドでCoCreateInstance(・・・)
をすると、「CoInitializeしてません」というエラーになった。
ともかく、COMを使うときは、最初に、スレッド毎に、これをする。
CoCloassを取得する(CoCreateInstance)
CoClassのCLSIDを指定して、インスタンスを取得する。
CoClassとは、ある機能をもつCOMインターフェースを、1つor複数実装している、COMを使うときの入口となるクラスという理解。
auto hr = CoCreateInstance(CLSID_SensorManager, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSensorManager));
第三引数は、
- COMのインプロセスサーバー(≒dll)である場合は
CLSCTX_INPROC_SERVER
- COMのアウトプロセスサーバー(≒exe)である場合は
CLSCTX_LOCAL_SERVER
を指定するという理解。
参考:https://eternalwindows.jp/com/combase/combase03.html
必要なインターフェースを、CoClassから取得(QueryInterface)
今回はやってないが、別の実験で作った下記のコードではこれをやっている。
入口のCoClassが、使いたい機能のためのインターフェースを実装しているかどうかをこれで確認して、実装していた場合は、そのインターフェースとしてのインスタンスを取得する、という理解。
取得したインターフェースがもつ関数を呼んで、目的を実現する
今回は、
ISensorManager
インターフェースが持つ、GetSensorsByCategory()
関数で、ISensorCollection
(センサーのリスト)を取得する
pSensorManager->GetSensorsByCategory(SENSOR_TYPE_AMBIENT_LIGHT, &pMotionSensorCollection);
センサーのリストで上がってきたセンサーの数を数える
pMotionSensorCollection->GetCount(&count);
センサーの数だけループさせ、センサーを取得する
for (int i = 0; i < count; i++) { pMotionSensorCollection->GetAt(i, &pAmbientSensor);
センサーの現在データを取得する
pAmbientSensor->GetData(&pData);
必要なデータを抽出する
pData->GetSensorValue(SENSOR_DATA_TYPE_LIGHT_LEVEL_LUX, &x);
ということをしている。
取得したインターフェース等のリソースを開放する
必要な解放してないかも。。。
→コード直す。(この辺りを拝見しながら。。。)
COMを終了する
CoInitialize(NULL);
したスレッドでは、COMを使い終わったらセットでこれが絶対必要。
CoUninitialize();
C#でやってみる
書いたコード全部
書いたコードは下記。
書いてみた感想としては、 おそらくC#で便利にCOMを扱えるようにするために、同じことをやるにも複数のやり方があるのだが、それが0から学ぶ上ですごく混乱した。
下記コードの途中、コメントアウトしてる部分があるが、それが、見つけた限りのその「複数のやり方」の部分。
そのあたりも、メモを残そうと思う。
using System.Runtime.InteropServices; namespace ISensorManagerCs { internal class Program { const string SENSOR_TYPE_AMBIENT_LIGHT = "97F115C8-599A-4153-8894-D2D12899918A"; const string SENSOR_DATA_TYPE_LIGHT_LEVEL_LUX = "E4C77CE2-DCB7-46E9-8439-4FEC548833A6"; static void Main(string[] args) { #if false var sensorManager = (ISensorManager)Activator.CreateInstance(CLSID.SensorManagerType)!; #else var sensorManager = new SensorManager(); #endif if (sensorManager is ISensorManager sm) { //sm.GetSensorsByCategory(new Guid(SENSOR_TYPE_AMBIENT_LIGHT), out var sc); var sc = sm.GetSensorsByCategory(new Guid(SENSOR_TYPE_AMBIENT_LIGHT)); if (sc is ISensorCollection c) { //var count = c.GetCount(out var count); var count = c.GetCount(); for (uint i = 0; i < count; i++) { //var sensor = sc.GetAt(i, out var sensor); var sensor = sc.GetAt(i); sensor.GetData(out var report); report.GetSensorValue(new PROPERTYKEY(new Guid(SENSOR_DATA_TYPE_LIGHT_LEVEL_LUX), 2), out var x); Console.WriteLine($"明るさは {x.fltVal} ルクスです"); } } } Console.ReadLine(); } #if false public static class CLSID { public static readonly Guid SensorManager = new Guid("77A1C827-FCD2-4689-8915-9D613CC5FA3E"); public static readonly Type SensorManagerType = Type.GetTypeFromCLSID(SensorManager)!; } #else // これがいわゆる「CoClass」だと思われる(たぶん) [ComImport] [Guid("77A1C827-FCD2-4689-8915-9D613CC5FA3E")] internal class SensorManager { } #endif [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("BD77DB67-45A8-42DC-8D00-6DCF15F8377A")] internal interface ISensorManager { //[PreserveSig] //public long GetSensorsByCategory([In] ref Guid sensorCategory, [Out] out ISensorCollection ppSensorsFound); public ISensorCollection GetSensorsByCategory(in Guid sensorCategory); // 以下略(使わないものは省略してOK) } [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("23571E11-E545-4DD8-A337-B89BF44B10DF")] internal interface ISensorCollection { //[PreserveSig] //public int GetAt([In] uint ulIndex, [Out] out ISensor ppSensor); public ISensor GetAt([In] uint ulIndex); //[PreserveSig] //public int GetCount([Out] out uint count); public uint GetCount(); } [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("5FA08F80-2657-458E-AF75-46F73FA6AC5C")] internal interface ISensor { [PreserveSig] public long GetID(ulong ulIndex, out Guid pID); [PreserveSig] public void dummy2(); [PreserveSig] public void dummy3(); [PreserveSig] public void dummy4(); [PreserveSig] public void dummy5(); [PreserveSig] public void dummy6(); [PreserveSig] public void dummy7(); [PreserveSig] public void dummy8(); [PreserveSig] public void dummy9(); [PreserveSig] public void dummy10(); [PreserveSig] public void GetData([Out] out ISensorDataReport ppDataReport); } [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("0AB9DF9B-C4B5-4796-8898-0470706A2E1D")] internal interface ISensorDataReport { [PreserveSig] public void dummy1(); [PreserveSig] public long GetSensorValue([In] PROPERTYKEY pKey, [Out] out PROPVARIANT pValue); } [StructLayout(LayoutKind.Explicit)] public struct PROPVARIANT { [FieldOffset(0)] public ushort vt; [FieldOffset(2)] public ushort wReserved1; [FieldOffset(4)] public ushort wReserved2; [FieldOffset(6)] public ushort wReserved3; [FieldOffset(8)] public IntPtr pwszVal; [FieldOffset(8)] public float fltVal; } [StructLayout(LayoutKind.Sequential, Pack =4)] public struct PROPERTYKEY { public Guid fmtid; public uint pid; public PROPERTYKEY(Guid key, uint id) { fmtid = key; pid = id; } } } }
やったこと
CoCloassを取得する
今回は、SensorManager
がそれにあたる。
やり方1:CoClassを定義して、それをnewする
C++のSensorApi.h
にあるこれを、
C#に定義する。
[ComImport] [Guid("77A1C827-FCD2-4689-8915-9D613CC5FA3E")] internal class SensorManager { }
で、定義したやつを、newする。
var sensorManager = new SensorManager();
これが、C++でいうところの下記に相当する。
auto hr = CoCreateInstance(CLSID_SensorManager, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSensorManager));
やり方2:Activator.CreateInstance()でとる
C++のSensorApi.h
にあるSensorManager
のCLSIDを、定義する。
そのGUIDから、SensorManager
のTypeを取得する。
public static class CLSID { public static readonly Guid SensorManager = new Guid("77A1C827-FCD2-4689-8915-9D613CC5FA3E"); public static readonly Type SensorManagerType = Type.GetTypeFromCLSID(SensorManager)!; }
そのTypeを使って、SensorManager
のインスタンスを取得する。
var sensorManager = (ISensorManager)Activator.CreateInstance(CLSID.SensorManagerType)!;
使うインターフェースを、C++の定義を見ながらC#に直す
今回は、下記をC#で定義する。
ISensorCollection
ISensor
ISensorDataReport
基本、C++のインターフェースに書かれている関数定義の順番に沿って、定義をしていく。 順番を勝手に入れ替えたり、飛ばしたりしてはいけない。
やり方1:前から順番に全部
まずはISensorCollection
で、C#での定義をやってみる。
もとになるC++のコードは下記。
C#に直す際は、順番が大事。勝手に関数の順番を並び替えてはいけない。
真面目に全部書くと、こうなる。
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("23571E11-E545-4DD8-A337-B89BF44B10DF")] internal interface ISensorCollection { [PreserveSig] public int GetAt([In] uint ulIndex, [Out] out ISensor ppSensor); //public ISensor GetAt([In] uint ulIndex); [PreserveSig] public int GetCount([Out] out uint count); [PreserveSig] public int Add([In] ISensor pSensor); [PreserveSig] public int Remove([In] ISensor pSensor); [PreserveSig] public int RemoveByID([In] REFSENSOR_ID sensorID); [PreserveSig] public int Clear(); }
※参考
もし、順番を間違えていたり、なにか定義がおかしいと、その関数を呼ぼうとしたところで下記のような例外が起きた。
型 'System.AccessViolationException' のハンドルされていない例外が ISensorManagerCs.dll で発生しました
やり方2:前から順番に書いていく(使わない関数は飛ばす)
今回、ISensorCollection
を使うが、その中で、実際に使っているのはGetAt
とGetCount
のみ。
そういう場合、使う関数の一番後ろのもの、今回だとGetCount
より後ろは、定義しなくてOK。
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("23571E11-E545-4DD8-A337-B89BF44B10DF")] internal interface ISensorCollection { [PreserveSig] public int GetAt([In] uint ulIndex, [Out] out ISensor ppSensor); //public ISensor GetAt([In] uint ulIndex); [PreserveSig] public int GetCount([Out] out uint count); }
これでOK。ただし、使う関数が一番後ろにあったりすると、そこまでに出てくる関数は、書くだけ書かないといけない。ただし、真面目に書かなくても、関数名やシグネチャは適当でもOK。
今回だと、ISensorCollection
は前2つだけ使うが、ISensor
のほうは、一番後ろ(11番目)のGetData
だけ使うので、前10関数分は適当に書いてOK。
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("5FA08F80-2657-458E-AF75-46F73FA6AC5C")] internal interface ISensor { [PreserveSig] public void dummy1(); [PreserveSig] public void dummy2(); [PreserveSig] public void dummy3(); [PreserveSig] public void dummy4(); [PreserveSig] public void dummy5(); [PreserveSig] public void dummy6(); [PreserveSig] public void dummy7(); [PreserveSig] public void dummy8(); [PreserveSig] public void dummy9(); [PreserveSig] public void dummy10(); [PreserveSig] public void GetData([Out] out ISensorDataReport ppDataReport); }
こんな感じ。
やり方3:VtblGap1_Xを使う
途中の、使わない関数を全部書くのが大変なので、省略した書き方もできる。
下記のようにする。
_VtblGap1_10
の「10」の部分が、省略する関数の個数。
(「1」の部分が何を指すのか、まだ理解できてない)
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("5FA08F80-2657-458E-AF75-46F73FA6AC5C")] internal interface ISensor { void _VtblGap1_10(); [PreserveSig] public void GetData([Out] out ISensorDataReport ppDataReport); }
※使う関数、使わない関数10個、使う関数、みたいな並びの時も、間の10個を飛ばすときにも使える。
詳細はこちら。
https://blog.shibayan.jp/entry/20151019/1445216164
シグネチャを、C#で使いやすいように変えることができる
ISensorCollection
のC++での定義は、上の「やり方1:前から順番に全部」のところに上げたもので、
C#での定義として今回作ったものは、たとえば、「やり方2:前から順番に書いていく(使わない関数は飛ばす)」に書いたようなものだった。
そのC#で書いたインターフェース定義を、
[PreserveSig]
を付ける、付けない、で、シグネチャをちょっと変える(C#的に便利にする)ことができる。
[PreserveSig]
についてはこちら。
PreserveSig =trueにして、そのままのシグネチャにする方法
PreserveSig =true とは、[PreserveSig ]
を付ける、ということ。(PreserveSig=true、という書き方もできるが)
コードは、「やり方2:前から順番に書いていく(使わない関数は飛ばす)」 に書いたように、下記になる。
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("23571E11-E545-4DD8-A337-B89BF44B10DF")] internal interface ISensorCollection { [PreserveSig] public int GetAt([In] uint ulIndex, [Out] out ISensor ppSensor); [PreserveSig] public int GetCount([Out] out uint count); }
- 関数の成功失敗は、戻り値のHRESULTで帰ってくる。
- 関数の出力は、ポインタの引数で帰ってくる。
ということになる。
PreserveSig =falseにして、引数のoutを戻り値に持ってくる方法
PreserveSig =false とは、[PreserveSig]
を付けない、ということ。
その場合、下記のように書くことができる。
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("23571E11-E545-4DD8-A337-B89BF44B10DF")] internal interface ISensorCollection { public ISensor GetAt([In] uint ulIndex); public uint GetCount(); }
つまり、
- 関数の成功失敗は、例外の有無になる。
- 関数の出力は、戻り値で帰ってくる。
という感じになる。
個人的には、PreserveSig =false
のほうが、C#では使いやすいなと感じた。
関数の引数で使う型を定義する
今回は、下記を定義する。これらには、GUIDの指定とかは不要。(だが、ただただめんどくさい)
PROPVARIANT
PROPERTYKEY
[StructLayout(LayoutKind.Explicit)] public struct PROPVARIANT { [FieldOffset(0)] public ushort vt; [FieldOffset(2)] public ushort wReserved1; [FieldOffset(4)] public ushort wReserved2; [FieldOffset(6)] public ushort wReserved3; [FieldOffset(8)] public IntPtr pwszVal; [FieldOffset(8)] public float fltVal; } [StructLayout(LayoutKind.Sequential, Pack =4)] public struct PROPERTYKEY { public Guid fmtid; public uint pid; public PROPERTYKEY(Guid key, uint id) { fmtid = key; pid = id; } }
特にPROPVARIANT
は、COMでは結構出てきそうな気がする。
元(C++)のPROPVARIANT
が共用体(union)を使ってるので、wReserved3
の後ろのpwszVal
やらfltVal
やらは、全部‘[FieldOffset(8)]‘の場所で定義するものと理解。
今回は、使うfltVal
以外は特別書かなかったので、その時必要なものだけ書けばよいかと思う。(モノによっては、また別途ややこしい構造体をかかないといけないっぽい)
関数の引数で使うGUIDを定義する
今回は、下記を定義する。
- 明るさセンサーのGUID
- データのタイプ指定(明るさ(LUX))のGUID
const string SENSOR_TYPE_AMBIENT_LIGHT = "97F115C8-599A-4153-8894-D2D12899918A"; const string SENSOR_DATA_TYPE_LIGHT_LEVEL_LUX = "E4C77CE2-DCB7-46E9-8439-4FEC548833A6";
InterfaceType(ComInterfaceType.InterfaceIsIUnknown) をつける
正直まだ理解しきれてないのだが、インターフェースの定義のところに書いている
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("23571E11-E545-4DD8-A337-B89BF44B10DF")] internal interface ISensorCollection
のInterfaceType(ComInterfaceType.InterfaceIsIUnknown)
はつけておいた方がよさそう。
これがないと、例外が起きたりはしなかったが、ISensorCollection
のGetCount()
で、センサーの数が0になって、うまく取れなかった。
完了
という感じで、いろいろ必要なインターフェースや構造体の定義をC#でしてあげて、 それを使って明るさセンサーの値を取るロジックを組んだのが、最初に上げたC#のコードになる。
ここまでで、残存の謎はあるものの、一通り今回やりたかったことはできたかと思う。
おそらく、知らないCOMの使い方やら謎やらがまだまだあると思われるが、それはまたそういうのにぶつかったときに勉強しようと思う。
メモ
C#でCOMを使おうと思うと、まずC++でコードを書いて、実現できることを確認して、それからC#をおこさないと、最初からC#は厳しいなと思った。 (C++で書いて、CLSIDやインターフェースの定義をF12で確認しつつ、それをC#コードに直していく、をしないと、C#でいきなり書くと、CLSIDやインターフェースの定義がどうなってるかを確認するのが大変)
今回、やろうと思ったことをすでに実現しているOSSがあったので、それのコードをだいぶ参考にさせてもらった。ありがとうございます。
https://github.com/dahall/vanara
参考
EternalWindows
COMの勉強するなら、ともかくここを見たらよいと思う。
https://eternalwindows.jp/index.html#com
あかるさセンサーの情報
https://learn.microsoft.com/en-us/windows/win32/sensorsapi/sensor-category-light
DllImportAttribute.PreserveSig Field
PreserveSigについて。
_VtblGap1_XX の書き方について
PROPVARIANT
https://learn.microsoft.com/ja-jp/windows/win32/api/propidl/ns-propidl-propvariant
COM Interop の基本を IShellLink を使って学びなおした(しばやん雑記)
https://blog.shibayan.jp/entry/20151019/1445216164
Windows Property System を使って C# から曲情報を取得する(しばやん雑記)
PROPVARIANTの変換?
https://blog.shibayan.jp/entry/20220504/1651658855
CsWin32 で Win32 API や COM を使ったアプリケーション開発を効率化する(しばやん雑記)
https://blog.shibayan.jp/entry/20220501/1651339430
以前、COMを勉強したつもりだった自分の記事