RustでCOMをやる - windows-rs 0.48.0版
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
じゃないとダメなやつもいるので二段構えにしています。
PCWSTR
はHSTRING
からやるのがなんか一番楽な気がするのでそうしています。
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
が得られるのでIDispatch
にcast
します。
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に対して次のようなことをやるとできます。
- メソッドやプロパティの名前からそのIDを得る
- メソッドやプロパティに渡すパラメータを作る
- VARIANTを良い感じにする
- 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::GetTypeInfo
でITypeInfo
が得られ、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_BYREF
なVARIANT
を作って引数として渡す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が落ちるんですよね。原因がわからん。
以下やったこと。
-
イベントハンドラの準備
- イベント処理用のIDispatchとなるstructを用意する
- そのstructに
#[implement(IDispatch)]
するuse windows::core::implement
- さらに
impl IDispatch_Impl
でInvoke
を実装する- ほかのメソッドは
unimplemented!()
- ほかのメソッドは
-
イベントハンドラのセット
-
IDispatch
からITypeInfo
を得る -
ITypeInfo
からイベントのあるインターフェース名に該当するITypeInfo
を得る - イベントインタフェースの
ITypeInfo
からriidを得る - 元の
IDispatch
からIConnectionPointContainerを得る - イベント処理用structを
IDispatch
にする -
IConnectionPointContainer::FindConnectionPoint
でriidからIConnectionPoint
を得る - イベント処理用
IDispatch
でIConnectionPoint::Advise
する…ここで突然の死!
-
古いバージョンのwindows-rsではできてたはずなんですけどねぇ…まぁその頃はイベントが発生しても反応しないとかいう別の問題抱えてたんですが。
その後以下のようなことを試しています。
- イベント処理用structを
IUnknown
にしてからAdvise
: やはり失敗する - イベント処理用structを
IDispatch
にしてからIUnknown
にする: そもそもこれがコケるのでAdvise
できないのかも? - イベント処理用structを
IUnknown
にしてからIDispatch
にする: あまり関係なさそうだけどこれもできないんだ - イベント処理用structを
IDispatch
にしてからInvokeを自分で叩く: ここも失敗する!そもそもInvoke
できないんじゃどうしようもない…
というわけでイベントはうまくいかなかったという話でした。
実際に動くやつ
Discussion