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 を呼び出す方法でした。
ではでは(╹⌓╹ )