SensorManagerで、C++とC#でCOMを使う練習をする その2

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

以前の記事で、COMをC++C#から使う練習をしたつもりだったが、再度COMを使うことになったときに、全然わかってなかったことに気づいてしまったので、もうちょっと頑張ってCOMを使う練習をしたい。

※既存のCOMのインターフェースを使う(COMクライアント)のを練習する。COMを作る(COMサーバー)側はまた今度、、、、

前提

  • VisualStudio2022
  • C++17
  • C#10

勉強しながらのメモ書きなので、誤りあるかも。

「~~という理解。」とか書いてるところは、特に自信ないところ。

書きたい内容

  • 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)

今回はやってないが、別の実験で作った下記のコードではこれをやっている。

https://github.com/tera1707/COM_JikkenWithSensor/blob/45d20400d9ba262f0d5398327111207a1b52f5c5/IShellLinkJikken/IShellLinkJikken.cpp#L38

入口の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++でかいたコードを、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を使うが、その中で、実際に使っているのはGetAtGetCountのみ。

そういう場合、使う関数の一番後ろのもの、今回だと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://learn.microsoft.com/ja-jp/dotnet/framework/unmanaged-api/metadata/imetadataemit-definemethod-method#slots-in-the-v-table

https://blog.shibayan.jp/entry/20151019/1445216164

シグネチャを、C#で使いやすいように変えることができる

ISensorCollectionC++での定義は、上の「やり方1:前から順番に全部」のところに上げたもので、 C#での定義として今回作ったものは、たとえば、「やり方2:前から順番に書いていく(使わない関数は飛ばす)」に書いたようなものだった。

そのC#で書いたインターフェース定義を、 [PreserveSig]を付ける、付けない、で、シグネチャをちょっと変える(C#的に便利にする)ことができる。

[PreserveSig]についてはこちら。

https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute.preservesig?view=net-7.0#remarks

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);
}

つまり、元(C++)のシグネチャと同じで

  • 関数の成功失敗は、戻り値の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)はつけておいた方がよさそう。

これがないと、例外が起きたりはしなかったが、ISensorCollectionGetCount()で、センサーの数が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について。

https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute.preservesig?view=net-7.0

_VtblGap1_XX の書き方について

https://learn.microsoft.com/ja-jp/dotnet/framework/unmanaged-api/metadata/imetadataemit-definemethod-method#slots-in-the-v-table

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を勉強したつもりだった自分の記事

https://tera1707.com/entry/2022/08/09/005543