📘

RustでCOMをやる - windows-rs 0.48.0版

2023/07/20に公開

RustでCOMオブジェクトを扱おうという試みです。

Cargo.toml

COMをやるのに使いそうなfeatureは以下です。

[dependencies.windows]
version = "0.48.0"
features = [
    "Win32_Foundation",
    "Win32_System_Com",
    "Win32_System_Ole",
]

初期化と解除

COMっぽいことをする前に CoInitializeEx で初期化する必要があります。初期化の際にCOINIT_MULTITHREADEDまたはCOINIT_APARTMENTTHREADEDといった定数を指定しますが、MTAとかSTAとかそういうのがよくわかっていないのでやってみて不都合がければまぁいいかなくらいの気持ちでやっています…。

まずは初期化。
windows-rsの関数は原則unsafeなのでunsafeしてください。

use windows::Windows::Win32::System::Com::{
    CoInitializeEx, COINIT_MULTITHREADED,
};

unsafe {
    CoInitializeEx(None, COINIT_MULTITHREADED)?;
}

そして解除(Uninitializeって解除でいいんですかね?)です。COMを使う用事が終わったら解除しておきましょう。

use windows::Windows::Win32::System::Com::{
    CoUninitialize,
};
unsafe {
    CoUninitialize();
}

COMオブジェクトを作る

CoCreateInstanceでCLSIDからIDispatchを得ます。CLSIDはProgIDやCLSID文字列から得られます。指定する定数はだいたいCLSCTX_ALLでいいんだけどExcelだとかCLSCTX_LOCAL_SERVERじゃないとダメなやつもいるので二段構えにしています。
PCWSTRHSTRINGからやるのがなんか一番楽な気がするのでそうしています。

use windows::{
    core::{HSTRING},
    Win32::System::{
        Com::{
            IDispatch,
            CLSIDFromString, CoCreateInstance,
            CLSCTX_ALL, CLSCTX_LOCAL_SERVER,
        }
    }
};

unsafe {
    // &strやStringからHSTRINGを作る
    let lpsz = HSTRING::from(progid);
    // IntoParam<PCWSTR>には&HSTRINGを渡す
    let rclsid = CLSIDFromString(&lpsz)?;
    // CLSCTX_ALLとCLSCTX_LOCAL_SERVER両方でやる
    let disp: IDispatch = match CoCreateInstance(&rclsid, None, CLSCTX_ALL) {
        Ok(disp) => disp,
        Err(_) => {
            CoCreateInstance(&rclsid, None, CLSCTX_LOCAL_SERVER)?
        },
    };
}

実行中のオブジェクトを得る

Excelとか起動中のやつを触りたいケースですね、この場合はIUnknownが得られるのでIDispatchcastします。

use windows::{
    core::{HSTRING, ComInterface},
    Win32::System::{
        Com::{
            IDispatch,
            CLSIDFromString,
        },
        Ole::{
            GetActiveObject,
        }
    }
};

unsafe {
    let lpsz = HSTRING::from(progid);
    let rclsid = CLSIDFromString(&lpsz)?;
    let pvreserved = std::ptr::null_mut() as *mut std::ffi::c_void;
    let mut ppunk = None;
    GetActiveObject(&rclsid, pvreserved, &mut ppunk)?;
    let disp: Option<IDispatch> = match ppunk {
        Some(unk) => {
            // ComInterfaceをuseしておくことでcastが使える
            let disp = unk.cast()?;
            Some(disp)
        },
        None => None,
    };
}

メソッド実行やプロパティの読み書き

IDispatchに対して次のようなことをやるとできます。

  1. メソッドやプロパティの名前からそのIDを得る
  2. メソッドやプロパティに渡すパラメータを作る
    • VARIANTを良い感じにする
  3. IDとパラメータを渡して実行する

メソッドやプロパティ名からIDを得る

COMオブジェクトのメソッドやプロパティには必ずIDが振られている(はず)なのでそれをまず取得します。
IDispatch::GetIDsOfNamesを使います。IDispatchに生えてるメソッドもunsafeなので注意しましょう。

use windows::{
    core::{GUID, HSTRING, PCWSTR},
};
/// ユーザーのデフォルト言語であることを示す
const LOCALE_USER_DEFAULT: u32 = 0x400;

unsafe {
    let riid = GUID::zeroed();
    let hstring = HSTRING::from(name);
    let rgsznames = PCWSTR::from_raw(hstring.as_ptr());
    let cnames = 1;
    let lcid = LOCALE_USER_DEFAULT;
    let mut dispidmember = 0;

    disp.GetIDsOfNames(&riid, &rgsznames, cnames, lcid, &mut dispidmember)?;
}

うまくいくとdispidmemberにIDが入ります。

パラメータを作る

パラメータは

  • メソッドの引数
  • パラメータ付きプロパティのパラメータ
  • プロパティに代入する値

などです。これをDISPPARAMS構造体で作っておきます。それぞれ作り方が少しずつ異なります。

use windows::{
    Win32::System::{
        Com::{
            DISPPARAMS, VARIANT,
        },
        Ole::{
            DISPID_PROPERTYPUT,
        }
    }
};

以下ではこのあたりをuseしています

メソッドの引数を作る

Wscript.ShellのPopupを例にやってみましょう。Popupは4つの引数を取りますが、後ろ3つは省略可能です。

// pseudo
ws.Popup("ほげほげ", 0, "ふがふが")

が実行される感じにしていきます。
なお、実際にパラメータとして渡される値はVARIANT構造体である必要があるのですが、これについては後述します。

// これが実行時に渡すパラメータになります
let mut pdispparams = DISPPARAMS::default();

// ここでVARIANT型の配列を作っていきます。
// from_*()とか書いていますがこういう関数は存在しないので、今のところ雰囲気で読んでてください
let mut args = vec![
    VARIANT::from_str("ほげほげ"),
    VARIANT::from_i32(0),
    VARIANT::from_str("ふがふが"),
];

// 引数は逆順にします、なぜかは知らんがそういうもんだからです
args.reverse();

// VARIANT配列の長さとポインタを教えてやります
pdispparams.cArgs = args.len() as u32;
pdispparams.rgvarg = args.as_mut_ptr();
名前付き引数対応

COMには名前付き引数という概念があって

// pseudo
ws.Popup(Text := "ほげほげ", Title := "ふがふが")

というように指定した名前のパラメータにだけ値を渡すという仕組みがあります。
IDispatch::GetTypeInfoITypeInfoが得られ、ITypeInfo::GetNamesでパラメータ名が得られます。

use windows::core::BSTR;

unsafe {
    // ITypeInfoを得る
    let info = disp.GetTypeInfo(0, 0)?;

    // メソッドのパラメータ名などを受ける為のバッファ
    // ここに
    // 0: 関数名
    // 1: パラメータ0名
    // 2: パラメータ1名
    // ︙
    // n-1: パラメータn名
    // n: 戻り値名
    // が入る
    let mut rgbstrnames = vec![BSTR::new(); 100];
    let cmaxnames = rgbstrnames.len() as u32;
    let mut pcnames = 0;
    info.GetNames(dispidmember, rgbstrnames.as_mut_ptr(), cmaxnames, &mut pcnames)?;


    let mut pdispparams = DISPPARAMS::default();

    // 実際に引数として渡す値
    let mut args = vec![
        VARIANT::from_str("ほげほげ"),
        VARIANT::from_str("ふがふが"),
    ];
    // 逆順にします
    args.reverse();

    // 名前に対応したIDの配列を作ります
    // id_from_names関数は渡した名前をいい感じに比較してIDを返すやつだと思ってください
    // パラメータのIDは1つ目が0、2つ目が1…となっています
    let mut named_args: Vec<i32> = vec![
        id_from_names(&rgbstrnames, "Text")?,
        id_from_names(&rgbstrnames, "Title")?,
    ];
    // Popupの場合 [0, 2] となります

    // やはり逆順にします
    named_args.reverse();

    pdispparams.cArgs = args.len() as u32;
    pdispparams.rgvarg = args.as_mut_ptr();
    // 名前付き引数があることを教えます
    pdispparams.cNamedArgs = named_args.len() as u32;
    pdispparams.rgdispidNamedArgs = named_args.as_mut_ptr();

}

これで

  • Textという名前のパラメータに"ほげほげ"を渡す
  • Titleという名前のパラメータに"ふがふが"を渡す

という意味になります

// pseudo
ws.Popup("ほげほげ", Title := "ふがふが")

のように位置指定と名前付きの混在の場合は

    let mut args = vec![
        VARIANT::from_str("ほげほげ"),
        VARIANT::from_str("ふがふが"),
    ];
    args.reverse();
    let mut named_args: Vec<i32> = vec![
        id_from_names(&rgbstrnames, "Title")?,
    ];
    named_args.reverse();

とやると

  • "ほげほげ"を0番目に渡す
  • Titleという名前のパラメータに"ふがふが"を渡す

になります。

プロパティの値を得る

値を得るだけなら空のDISPPARAMSを作ります。

let pdispparams = DISPPARAMS::default();

プロパティへの代入

代入する値とそれが代入される値なんですよ、ということを教えてあげる必要があります。
WScript.ShellのCurrentDirectoryを例にやってみます。

// pseudo
ws.CurrentDirectory = "C:\HogeHoge"
let pdispparams = DISPPARAMS::default();
let mut args = vec![
    VARIANT::from_str("C:\\HogeHoge"),
];
let mut named_args = vec![DISPID_PROPERTYPUT];

pdispparams.cArgs = args.len() as u32;
pdispparams.rgvarg = args.as_mut_ptr();
pdispparams.cNamedArgs = named_args.len() as u32;
pdispparams.rgdispidNamedArgs = named_args.as_mut_ptr();

DISPID_PROPERTYPUTがプロパティに渡す値だと伝えるためのものですね。これにより渡すパラメータの値が代入されます。
名前付き引数のIDを指定するとこなんですが、このように特殊なパラメータ指定にも使われるようです。

パラメータ付きプロパティの値を得る

WScript.ShellのEnvironmentなんかがそうですね。

// pseudo
// ユーザー環境変数一覧を得る
userenv = ws.Environment("User")

パラメータの名前を渡していきます

let mut pdispparams = DISPPARAMS::default();

let mut args = vec![
    VARIANT::from_str("User"),
];

args.reverse();

pdispparams.cArgs = args.len() as u32;
pdispparams.rgvarg = args.as_mut_ptr();

パラメータ付きプロパティへの代入

代入する値とプロパティのパラメータの両方を教えます。
具体例がパッと思いつかなかったので雰囲気でやっていきます。

// pseudo
foo.Bar("hoge") = "ほげほげ"
let mut pdispparams = DISPPARAMS::default();

let mut args = vec![
    VARIANT::from_str("ほげほげ"), // 代入する値
    VARIANT::from_str("hoge"),     // パラメータ名
];
args.reverse();
let mut named_args = vec![DISPID_PROPERTYPUT];

pdispparams.cArgs = args.len() as u32;
pdispparams.rgvarg = args.as_mut_ptr();
pdispparams.cNamedArgs = named_args.len() as u32;
pdispparams.rgdispidNamedArgs = named_args.as_mut_ptr();

これでプロパティの"hoge"に"ほげほげ"が代入されます

実行する

メソッド・プロパティのIDとDISPPARAMSがあればIDispatch::Invokeで処理を行えます。wflagsという引数にDISPATCH_FLAGSを渡すことでどういった処理を行うかを指定します。メソッド・プロパティとこのフラグがミスマッチだとエラーになります。

use windows::{
    core::{GUID},
    Win32::System::{
        Com::{
            VARIANT, EXCEPINFO,
            DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT, DISPATCH_METHOD,
        },
    }
}; 
// システムのデフォルトロケールを示す
const LOCALE_SYSTEM_DEFAULT: u32 = 0x0800;

unsafe {
    let riid = GUID::zeroed();
    let lcid = LOCALE_SYSTEM_DEFAULT;
    // これで戻り値を受けます
    let mut pvarresult = VARIANT::default();
    // エラー発生時の詳細をこれで得ます
    let mut pexcepinfo = EXCEPINFO::default();
    // 引数に問題があった場合に何番目の引数が悪かったかわかる?んですかね?よくわかってません
    let mut puargerr = 0;

    // なにやるかを教える
    let wflags = DISPATCH_METHOD;         // メソッドを実行する場合
    // let wflags = DISPATCH_PROPERTYGET; // プロパティの値を得る場合
    // let wflags = DISPATCH_PROPERTYPUT; // プロパティに値を代入する場合

    // プロパティ取得とメソッド実行は似たりよったりなのでこうしちゃってもいい
    // let wflags = DISPATCH_PROPERTYGET|DISPATCH_METHOD;

    disp.Invoke(
        dispidmember, // メソッド・プロパティのID
        &riid,
        lcid,
        wflags,
        &pdispparams, // DISPPARAMS構造体
        Some(&mut pvarresult), // 戻り値
        Some(&mut pexcepinfo), // エラー詳細、不要ならNoneでいい
        Some(&mut puargerr),   // こっちも不要ならNoneでいい
    )?;

}

うまくいくとpvarresultに戻り値が入ります、これもVARIANTなのでRust側で使うにはよろしくする必要があります。

VARIANTをよろしくやる

VARIANTを扱いやすくするためにtraitを書きます。

use windows::{
    core::{self},
    Win32::System::{
        Com::{
            VARIANT,
        },
    }
}; 

trait VariantExt {
    fn null() -> VARIANT;
    fn from_i32(n: i32) -> VARIANT;
    fn from_str(s: &str) -> VARIANT;
    fn from_bool(b: bool) -> VARIANT;
    fn to_i32(&self) -> core::Result<i32>;
    fn to_string(&self) -> core::Result<String>;
    fn to_bool(&self) -> core::Result<bool>;
}

ほんとはもっとね、使ってると色々欲しくなるんですがとりあえずこんな感じでやっていきます。
もともとUnion使われまくってるせいで結構めんどい感じになっていますが、こうしてtraitの実装さえできちゃえば気楽に扱えます。
では実装を書きましょう。

use std::mem::ManuallyDrop;
use windows::{
    core::{self, BSTR},
    Win32::{
        Foundation::VARIANT_BOOL,
        System::{
            Com::{
                VARIANT, VARIANT_0_0,
                VT_NULL, VT_BSTR, VT_BOOL, VT_I4,
            },
            Ole::{
                VariantClear,
            }
        }
    }
}; 

impl VariantExt for VARIANT {
    fn null() -> VARIANT {
        let mut variant = VARIANT::default();
        let mut v00 = VARIANT_0_0::default();
        v00.vt = VT_NULL;
        variant.Anonymous.Anonymous = ManuallyDrop::new(v00);
        variant
    }
    fn from_i32(n: i32) -> VARIANT {
        let mut variant = VARIANT::default();
        let mut v00 = VARIANT_0_0::default();
        v00.vt = VT_I4;
        v00.Anonymous.lVal = n;
        variant.Anonymous.Anonymous = ManuallyDrop::new(v00);
        variant
    }
    fn from_str(s: &str) -> VARIANT {
        let mut variant = VARIANT::default();
        let mut v00 = VARIANT_0_0::default();
        v00.vt = VT_BSTR;
        let bstr = BSTR::from(s);
        v00.Anonymous.bstrVal = ManuallyDrop::new(bstr);
        variant.Anonymous.Anonymous = ManuallyDrop::new(v00);
        variant
    }
    fn from_bool(b: bool) -> VARIANT {
        let mut variant = VARIANT::default();
        let mut v00 = VARIANT_0_0::default();
        v00.vt = VT_BOOL;
        v00.Anonymous.boolVal = VARIANT_BOOL::from(b);
        variant.Anonymous.Anonymous = ManuallyDrop::new(v00);
        variant
    }
    fn to_i32(&self) -> core::Result<i32> {
        unsafe {
            let mut new = VARIANT::default();
            VariantChangeType(&mut new, self, 0, VT_I4)?;
            let v00 = &new.Anonymous.Anonymous;
            let n = v00.Anonymous.lVal;
            VariantClear(&mut new)?;
            Ok(n)
        }
    }
    fn to_string(&self) -> core::Result<String> {
        unsafe {
            let mut new = VARIANT::default();
            VariantChangeType(&mut new, self, 0, VT_BSTR)?;
            let v00 = &new.Anonymous.Anonymous;
            let str = v00.Anonymous.bstrVal.to_string();
            VariantClear(&mut new)?;
            Ok(str)
        }
    }
    fn to_bool(&self) -> core::Result<bool> {
        unsafe {
            let mut new = VARIANT::default();
            VariantChangeType(&mut new, self, 0, VT_BOOL)?;
            let v00 = &new.Anonymous.Anonymous;
            let b = v00.Anonymous.boolVal.as_bool();
            VariantClear(&mut new)?;
            Ok(b)
        }
    }
}

こうやっとけば上の方で書いたVARIANT::from_str("ほげほげ")とか実際にできるようになるんですよ、便利ですね。

見ての通りVARIANTは中でManuallyDropが使われてるので自分でちゃんと自分でdropしてやらんといかんのですが、それにはVariantClearすれば良いよって話なのでなんかそういう仕組を作っても良さそうですね

use windows::{
    core::{self},
    Win32::System::{
        Com::{
            VARIANT, VARIANT_0_0,
            VT_NULL, VT_BSTR, VT_BOOL, VT_I4,
        },
        Ole::{
            VariantClear,
        }
    }
}; 

// VARIANTのラッパー構造体を作る
struct Variant(VARIANT);

// drop時にVariantClearを呼ぶ
impl Drop for Variant {
    fn drop(&mut self) {
        unsafe {
            let _ = VariantClear(&mut self.0);
        }
    }
}

ByRef

いわゆる参照渡しってやつで、メソッドの引数から値を渡しつつその引数で別の値を受けるみたいなのがね、できる仕組みがあります。

// pseudo
sc = createoleobj("ScriptControl")
sc.language = "VBScript"
script = "
Function Hoge(ByRef n)
    n = n * 2 ' 2倍にする
End Function
"
sc.ExecuteStatement(script)

n = 50
sc.CodeObject.Hoge(ref n)
print(n) // 100

こういうのですね。
この場合VT_VARIANT|VT_BYREFVARIANTを作って引数として渡すVARIANTを参照させておくのが良さそう?です。

use std::mem::ManuallyDrop;
use windows::{
    Win32::System::{
        Com::{
            VARIANT, VARIANT_0_0,
            VARENUM, VT_VARIANT, VT_BYREF,
        },
    }
}; 

// 引数として渡したい値
let mut vaiant = VARIANT::from_i32(50);

// ByRefなVARIANTを別に作る
let mut byref = VARIANT::default();
let mut v00 = VARIANT_0_0::default();
v00.vt = VARENUM(VT_VARIANT.0|VT_BYREF.0) // まだVARENUMにBitOrが実装されてないのでこうなります
v00.Anonymous.pvarVal = &mut variant;
byref.Anonymous.Anonymous = ManuallyDrop::new(v00);

SAFEARRAY

VARIANTで配列を使いたい場合、つまりVT_ARRAYの場合はSAFEARRAYを作る必要があります。

use std::ffi::c_void;
use windows::{
    core::{self},
    Win32::System::{
        Com::{
            SAFEARRAY, SAFEARRAYBOUND,
            VARIANT, VARIANT_0_0,
            VARENUM, VT_VARIANT, VT_ARRAY,
        },
        Ole::{
            SafeArrayCreate, SafeArrayPutElement,
        }
    }
}; 

unsafe {
    // SAFEARRAYを作る

    // 配列要素の型を指定、みんなVARIANT型ということにしておけばとりあえずなんでも入るようになるよ
    let vt = VT_VARIANT
    // 配列の次元数
    let cdims = 1;
    // 次元ごとの配列サイズと始点
    // cdimsの数だけSAFEARRAYBOUNDを作る
    let rgsabound = vec![
        SAFEARRAYBOUND {
            // 要素数
            cElements: 3,
            // インデックスの始点、1から始まったり-1から始まったりする配列も作れる
            lLbound: 0
        }
    ];

    // どっかしらんとこに作られてポインタが返されるのであとでちゃんと始末しないといけないよ
    let psa: *mut SAFEARRAY = SafeArrayCreate(vt, cdims, rgsabound.as_ptr());

    // 各要素の値を作っておく
    let elems = vec![
        VARIANT::from_str("hoge"),
        VARIANT::from_i32(123),
        VARIANT::from_bool(true),
    ];

    // 要素をセット
    for (i, elem) in elems.iter().enumerate() {
        // 値のポインタはc_voidにする
        let pv = elem as *const VARIANT as *const c_void;
        // 次元ごとのインデックスを示す配列、2次元の場合は [0, 0] とかになる
        let rgindices = vec![*i as i32];

        SafeArrayPutElement(psa, rgindices.as_ptr(), pv)?;
    }

    // VARIANTにする
    let mut array = VARIANT::default();
    let mut v00 = VARIANT_0_0::default();
    v00.vt = VARENUM(VT_VARIANT.0|VT_ARRAY.0)
    v00.Anonymous.parray = psa;
    array.Anonymous.Anonymous = ManuallyDrop::new(v00);

}

イベントがうまくいきませんでした

イベントやったけど何かしらエラーでexeが落ちるんですよね。原因がわからん。
以下やったこと。

  • イベントハンドラの準備

    1. イベント処理用のIDispatchとなるstructを用意する
    2. そのstructに#[implement(IDispatch)]する
      • use windows::core::implement
    3. さらにimpl IDispatch_ImplInvokeを実装する
      • ほかのメソッドはunimplemented!()
  • イベントハンドラのセット

    1. IDispatchからITypeInfoを得る
    2. ITypeInfoからイベントのあるインターフェース名に該当するITypeInfoを得る
    3. イベントインタフェースのITypeInfoからriidを得る
    4. 元のIDispatchからIConnectionPointContainerを得る
    5. イベント処理用structをIDispatchにする
    6. IConnectionPointContainer::FindConnectionPointでriidからIConnectionPointを得る
    7. イベント処理用IDispatchIConnectionPoint::Adviseする…ここで突然の死!

古いバージョンのwindows-rsではできてたはずなんですけどねぇ…まぁその頃はイベントが発生しても反応しないとかいう別の問題抱えてたんですが。
その後以下のようなことを試しています。

  • イベント処理用structをIUnknownにしてからAdvise: やはり失敗する
  • イベント処理用structをIDispatchにしてからIUnknownにする: そもそもこれがコケるのでAdviseできないのかも?
  • イベント処理用structをIUnknownにしてからIDispatchにする: あまり関係なさそうだけどこれもできないんだ
  • イベント処理用structをIDispatchにしてからInvokeを自分で叩く: ここも失敗する!そもそもInvokeできないんじゃどうしようもない…

というわけでイベントはうまくいかなかったという話でした。

実際に動くやつ

https://github.com/stuncloud/rust-com-object

GitHubで編集を提案

Discussion