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

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

COM系記事

https://tera1707.com/entry/2023/10/15/012134#COM

やりたいこと

仕事で、COMを扱う機会があった。

そこで、C++で書かれたCOMを使うコードを、C#に書き直す、ということをした。
その時のメモ。

やったこと

とにかく、「COM windows C#」とかで検索しても、それっぽい情報が出てこない。

今回、仕事では上記やりたいこと(COMでセンサーを扱うということ)が何とかできたのだが、復習のために自宅PCで仕事でやったことを思い出してやってみようとしても、自宅PCにセンサ-類がなにもついていないためか、思った動きに全然なってくれない。
(センサーが一つも見つからない)

で、それが、PCに本当にセンサーが1つもないから仕方ないことなのか、自分のコードの書き方が間違えているせいでセンサーをとれないのか、の切り分けがどうしてもできなかった。

いつまでもぐるぐる回る感じになったので、書いたことをちゃんと動かして確認はいったんあきらめて、COMのコードをC++C#に直す部分を、覚えているうちにメモしようと思う。

使わせていただいたコード

下記の、COMでセンサーを扱うコードの一部を参考にさせて頂き、C#に直した。

blog.okazuki.jp

C#でのCOMのコードの書き方は、下記ページの「手動でinterfaceを用意する方法」の部分を参考にした。

ichigopack.net

コード(C++

C++で書いたコード(とりあえずなにかのセンサー(今回は加速度センサ)を見つけようとするところまで)
これをC#に直す。

#include <windows.h>
#include <iostream>
//#include <propvarutil.h>
#include <SensorsApi.h>
#include <sensors.h>
#pragma comment(lib, "Sensorsapi.lib")

int main()
{
    std::cout << "Hello World!\n";

    CoInitialize(NULL);

    ISensorManager *pSensorManager;
    ISensorCollection* pMotionSensorCollection;
    ISensor* pMotionSensor;

    if (!SUCCEEDED(::CoCreateInstance(CLSID_SensorManager, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pSensorManager))))
    {
        return 0;
    }
    if (!SUCCEEDED(pSensorManager->GetSensorsByCategory(SENSOR_TYPE_ACCELEROMETER_1D, &pMotionSensorCollection)))
    {
        pSensorManager->Release();
        return 0;
    }
    if (!SUCCEEDED(pMotionSensorCollection->GetAt(0, &pMotionSensor)))
    {
        pMotionSensorCollection->Release();
        pSensorManager->Release();
        return 0;
    }

    CoUninitialize();
    return 1;
}

コード(C#)

C#に直したコード。

C++でやってるCoInitialize(NULL);にあたるコードは、C#のほうにはいらない。

using System.Runtime.InteropServices;

namespace ComJikkenNet6_
{
    internal class Program
    {
        const string SENSOR_TYPE_ACCELEROMETER_1D = "C04D2387-7340-4CC2-991E-3B18CB8EF2F4";

        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");

            var sensorManager = new SensorManager();
            if(sensorManager is ISensorManager sm)
            {
                sm.GetSensorsByCategory(new Guid(SENSOR_TYPE_ACCELEROMETER_1D), out var sc);

                sc.GetAt(0, out var sensor);

            }

        }
    }
}

// これがいわゆる「CoClass」だと思われる(たぶん)
[ComImport]
[Guid("77A1C827-FCD2-4689-8915-9D613CC5FA3E")]
internal class SensorManager
{
}

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("BD77DB67-45A8-42DC-8D00-6DCF15F8377A")]
internal interface ISensorManager
{
    [PreserveSig]
    public long GetSensorsByCategory(Guid sensorCategory, out ISensorCollection ppSensorsFound);

    // 以下略(使わないものは省略してOK)
}

[ComImport]
[Guid("23571E11-E545-4DD8-A337-B89BF44B10DF")]
internal interface ISensorCollection
{
    [PreserveSig]
    public long GetAt(ulong ulIndex, out ISensor ppSensor);

}

[ComImport]
[Guid("5FA08F80-2657-458E-AF75-46F73FA6AC5C")]
internal interface ISensor
{
    [PreserveSig]
    public long GetID(ulong ulIndex, out Guid pID);
}

C#に直すときのポイント

CoClassの定義

CLSID_SensorManagerの定義を、C++のコード上で探す。 →CLSID_SensorManager の上でF12を押す。

こんな感じで、CLSID_SensorManagerのCLSID定義と、SensorManagerクラスの定義が出てくる。

そのクラスの定義を、下記のようにC#に持ってくる。 クラスの中身はいらない。

[ComImport]
[Guid("77A1C827-FCD2-4689-8915-9D613CC5FA3E")]
internal class SensorManager
{
}

CLSID_SensorManagerをCoCreateInstance()するところを、C#にするには、SensorManagerをnewするだけでいい。

var sensorManager = new SensorManager();

CoClassの定義 別解

作業中・・・

Interfaceの取り込み

次に、ISensorManagerのインターフェースを取り込む。

ISensorManagerは、C++ではF12を押すと、下記のように定義されてるのが見れる。

それをC#にもってくると、下記のようになる。

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("BD77DB67-45A8-42DC-8D00-6DCF15F8377A")]
internal interface ISensorManager
{
    [PreserveSig]
    public long GetSensorsByCategory(Guid sensorCategory, out ISensorCollection ppSensorsFound);

    // 以下略(使わないものは省略してOK)
}

GUIDはそのまま持ってくる。

メソッドの並びについて

C++IFの定義の中で、関数が4つほどあるが、この順番は勝手に変えてはいけない。 順番が重要。
COMがどこからでも(C++でもC#でも、エクセルでもVBでもどこでも)呼べるようにする都合で、順番とか大きさ?が重要らしい。

なので、ISensorManagerの場合、一つ目の関数は必ずGetSensorsByCategoryにする。

また、自分のC#コードの中で、ISensorManagerの中で使う関数がGetSensorsByCategoryだけの場合、それ以降の関数はC#コードの方には書かなくてOK。 逆に、1つ目の関数は使わなくて、2つ目の関数だけ使うときは、1つ目の関数を真面目に書いてもいいが、「dummy()」とかにしてしまってもOK。(ただ今回ためさなかった)

今回のコードの中で、Interfaceを「ISensorManager」以外に「ISensorCollection」「ISensor」の2つを使うので、その2つも、ISensorManagerと同じようにC#コード側に定義する。

省略できるあれについて

void _VtblGap1_17();などと書く。

参考:https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/metadata/imetadataemit-definemethod-method#slots-in-the-v-table

作業中・・・

PreserveSigについて

これを付けると、HRESULTを戻すシグネチャではなく、帰ってくるハンドルを戻り値とするメソッドに変換できる。

などなど作業中・・・

その他・・・

SensorManagerをnewしたものを、ISensorManagerでキャストして、ISensorManagerのもつメソッドを読んでやれば、C++pSensorManager->GetSensorsByCategory(SENSOR_TYPE_ACCELEROMETER_1D, &pMotionSensorCollection)と同じことができる。

あとは、同じ要領で、Interfaceを使って関数を読んでいく。

おわり

これで、C++と同じ動きをC#でさせることができたはずなのだが、自宅PCでその確認ができなかった。
どこかの時点で、仕事PCにこのコードを持って行って試してみるか、自宅でセンサのあるPCを入手できるかしたら、動作確認してみることにする。

参考

COMの基礎

http://chokuto.ifdef.jp/urawaza/com/com.html

サンプルCOMクライアント

https://ichigopack.net/win32com/com_base_2_samplecode.html

MediaFoundation

https://docs.microsoft.com/ja-jp/windows/win32/medfound/media-foundation-and-com

よさげなサンプル?
これの関数一つ一つが、GUIDをもつIFの中の関数なのか?→C#で書くとしたら、いろいろかかなあかんやつ?

https://komugi-com.hatenadiary.org/entry/20120128/1327701429

手動でinterfaceを用意する方法

https://ichigopack.net/win32com/com_csharp_2.html

C#でISensorCollectionとかを定義するときに参考になりそう

https://github.com/dotMorten/Windows.Devices.Sensors/blob/master/src/Windows.Devices.Sensors/Interop/ISensorManager.cs

書きたいことメモ

  • CoClassの取得の仕方
    • newするパターン
    • Activator.CreateInstance(CLSID.SensorManagerType) するパターン
  • インターフェースのメソッドの宣言の仕方
    • 前から順番に書いてく方法
    • 使わないヤツは適当に書く場合
    • _VtblGap1_17 などと書く場合 -メソッドの宣言の仕方
    • PreserveSig =trueにして、そのままのシグネチャにする方法
    • PreserveSig =falseにして、引数のoutを戻り値に持ってくる方法
  • その他
    • , InterfaceType(ComInterfaceType.InterfaceIsIUnknown)は必ずつけましょう
      • なくてもIFの取得はできたが、outがintとかの場合(GetCountとか)に、うまく値が取れなかった