なつねこメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ 書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

Rust から Windows の COM を呼び出したい

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