主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ
書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。
2020/01/16
Rust から、 Windows の COM を呼び出したくなったので、呼び出してみました。
コードは、以下のリポジトリに置いてあります。
GitHub - mika-sandbox/rust-wallpaper
まずは Cargo.toml
に Microsoft の公式実装である com クレートを追加します。
[target.'cfg(windows)'.dependencies]
# crates.io のはちょっと古いので、 Git Repository から引っ張ってくる
com = { git = "https://github.com/microsoft/com-rs" }
# Windows API Binding の winapi も使うので追加しておきます
winapi = { version = "0.3" }
cfg(windows)
としているのは、 Windows 以外ではビルドできない (意味が無い) 為です。
次に、呼び出したい COM の Interface を定義します。
今回は壁紙関連機能を提供している IDesktopWallpaper を使用します。
IDesktopWallpaper の Interface 定義は以下のようになります。
use com::{com_interface, interfaces::iunknown::IUnknown};
#[com_interface("B92B56A9-8B55-4E14-9A89-0199BBB6F93B")]
pub trait IDesktopWallpaper: IUnknown {
fn set_wallpaper(&self);
fn get_wallpaper(&self);
fn get_monitor_device_path_at(&self);
fn get_monitor_device_path_count(&self);
fn get_monitor_rect(&self);
fn set_background_color(&self);
fn get_background_color(&self);
fn set_position(&self);
fn get_position(&self);
fn set_slideshow(&self);
fn get_slideshow(&self);
fn set_slideshow_options(&self);
fn get_slideshow_options(&self);
fn advance_slideshow(&self);
fn get_status(&self);
fn enable(&self);
}
COM Interface を定義する際、基本的には全ての関数を定義しておく必要があります。
ただし、今回は一番最初の set_wallpaper
のみを使用するので、
今回の場合はそれだけ定義しても問題ありません。
今回使用する set_wallpaper
は以下のように定義します。
use winapi::{shared::winerror::HRESULT, um::winnt::LPCWSTR};
fn set_wallpaper(&self, monitor_id: LPCWSTR, wallpaper: LPCWSTR) -> HRESULT;
次に、実際に IDesktopWallpaper のインスタンスを作成します。
まず、 IDesktopWallpaper を実装した DesktopWallpaper の IID を以下のように定義します。
use winapi::shared::guiddef::IID;
// C2CF3110-460E-4fc1-B9D0-8A1C0C9CC4BD
const CLSID_DESKTOP_WALLPAPER: IID = IID {
Data1: 0xC2CF3110,
Data2: 0x460E,
Data3: 0x4FC1,
Data4: [0xB9, 0xD0, 0x8A, 0x1C, 0x0C, 0x9C, 0xC4, 0xBD],
};
次に上記 IID でインスタンスを作成...と行きたいのですが、
この状態では「そんなものはないよ!」というエラーが出てしまいます。
これは、 com の内部でインスタンス作成時に、プロセス内部のもののみ呼び出せる
CLSCTX_INPROC_SERVER
を指定している為です。
そのため、メソッドを拡張して他の CLSCTX
にも対応したメソッドを追加してあげます。
私の場合は以下のようにして拡張しました。
use com::{
runtime::ApartmentThreadedRuntime as Runtime,
ComInterface, InterfacePtr, InterfaceRc
};
use winapi::{
ctypes::c_void,
shared::{
guiddef::{REFCLSID, REFIID},
minwindef::LPVOID,
},
um::{
combaseapi::CoCreateInstance,
unknownbase::LPUNKNOWN,
},
};
trait RuntimeExtensions {
fn create_instance_ext<T: ComInterface + ?Sized>(
&self,
clsid: &IID,
cls_ctx: u32,
) -> Result<InterfaceRc<T>, HRESULT>;
unsafe fn create_raw_instance_ext<T: ComInterface + ?Sized>(
&self,
clsid: &IID,
cls_ctx: u32,
outer: LPUNKNOWN,
) -> Result<InterfacePtr<T>, HRESULT>;
}
impl RuntimeExtensions for Runtime {
fn create_instance_ext<T: ComInterface + ?Sized>(
&self,
clsid: &IID,
cls_ctx: u32,
) -> Result<InterfaceRc<T>, HRESULT> {
unsafe {
Ok(InterfaceRc::new(self.create_raw_instance_ext::<T>(
clsid,
cls_ctx,
null_mut(),
)?))
}
}
unsafe fn create_raw_instance_ext<T: ComInterface + ?Sized>(
&self,
clsid: &IID,
cls_ctx: u32,
outer: LPUNKNOWN,
) -> Result<InterfacePtr<T>, HRESULT> {
let mut instance = null_mut::<c_void>();
let hr = CoCreateInstance(
clsid as REFCLSID,
outer,
cls_ctx,
&T::IID as REFIID,
&mut instance as *mut LPVOID,
);
if hr != 0 {
return Err(hr);
}
Ok(InterfacePtr::new(instance))
}
}
これでプロセス外部のものも任意で呼べるようになりました。
あとは、この拡張メソッドを使ってインスタンスを作成します。
use winapi::shared::wtypebase::CLSCTX_LOCAL_SERVER;
let runtime = Runtime::new().expect("Failed to initialize COM Library");
let wallpaper = runtime
.create_instance_exp::<dyn IDesktopWallpaper>(&CLSID_DESKTOP_WALLPAPER, CLSCTX_LOCAL_SERVER)
.unwrap_or_else(|hr| panic!("Failed to get DesktopWallpaper object: HRESULT={:x}", hr));
最後に、呼び出したいメソッドを呼び出してあげれば完成です。
use std::ptr::null_mut;
wallpaper.set_wallpaper(null_mut(), path.encode_utf16().chain(Some(0)).collect().as_ptr());
なお、開放処理については、ライブラリの内部にてオブジェクトが破棄されるタイミングで
自動的に IUnknown->Release
が呼ばれるようになっているのと、
Runtime が削除されるタイミングで CoUninitialize
も呼ばれるようになっているため、
明示的に行う必要はありません。
ということで、 Rust で COM を呼び出す方法でした。
ではでは(╹⌓╹ )