Rust で Excel オートメーション (windows-rs 版)

## 動機
下記の記事では [winapi](https://crates.io/crates/winapi) を使っていました。

https://qiita.com/benki/items/de2e104a5866fad0ebab

最近 Microsoft 製 windows crate もだいぶ熟してきたようなので書き直してみたいと思います。

**windows-rs を 0.29.0 → 0.39.0 にアップデートしました。文字列の扱いなど変更されています。(2022/08/12)**

## 環境

Windows 10 64bit
Excel 2016
rustc 1.63.0
[windows](https://crates.io/crates/windows) 0.39.0

## [windows](https://crates.io/crates/windows) crate と [windows-sys](https://crates.io/crates/windows-sys) crate の違い

windows-sys crate は Win32API の関数やら構造体をそのままエクスポートしているだけです。なので、関数の引数がワイド文字列の場合は、Rust の文字列を`encode-utf16`で UTF-16 に変換する必要があります。また、戻り値が`HRESULT`であったり、ポインタで値が返ってきたりするので Rust の世界で扱うにはちょっと面倒です。

一方で windows crate は Win32API を Rust っぽく書けるように、いろんな trait を実装しています。~~文字列の引数にはそのまま Rust の文字列を渡せますし~~（**0.39.0 では w! マクロを使用する**）、戻り値は`Result`に包まれて返ってきます！特別な理由がない限りは windows crate を使うべきでしょう。

例えば以下のような感じです。

```rust:windows-sysの場合
// Rust の世界の文字列を UTF-16 に変換
let mut id: Vec<u16> = "Excel.Application".encode_utf16().chain(Some(0)).collect();
// 結果を受け取るための変数。mem::zeroed の使用は非推奨
let mut clsid: GUID = std::mem::zeroed();
// 戻り値は HRESULT
let hr = CLSIDFromProgID(id.as_mut_ptr(), &mut clsid);
// エラーチェック
if hr < 0 {
    return Err(..);
}
```

```rust:windowsの場合
// 0.39.0。Rust の世界の文字列を w! マクロで変換する。
let clsid = CLSIDFromProgID(w!("Excel.Application"))?;
```

~~あと、std feature を有効にすることで`windows::core::Error`に`std::error::Error` trait が実装されるので、エラー処理に anyhow が使えるようになります。alloc feature を有効にすることで、`impl IntoParam<PWSTR> for &str`が有効になって Rust の文字列をそのまま関数に渡せるようになります。~~ **0.39.0 では std、alloc feature はなくなっています。**

```toml:Cargo.toml
[dependencies]
anyhow = "1.0"

[dependencies.windows]
version = "0.39"
features = [
    "Win32_System_Com",
    "Win32_Foundation",
]
```
```rust:main.rs
use anyhow::Result;
use windows::{w, Win32::System::Com::CLSIDFromProgID};

fn main() -> Result<()> {
    let _clsid = unsafe { CLSIDFromProgID(w!("Excel.Application"))? };
    Ok(())
}
```

すばらしい:relaxed:

## 構造体

windows crate では Win32API の構造体に Default trait が実装されています。winapi crate のときはヘルパー関数を使って構造体のメモリ領域を確保していましたが、windows crate なら `(構造体)::default();`って書くだけで良いです。いいね！

```rust:winapiまたはwindows-sysの場合
let variant = init::<VARIANT>();

fn init::<T>() -> T {
    std::mem::MaybeUninit::<T>::uninit().assume_init()
}
```

```rust:windowsの場合
let variant = VARIANT::default();
```

2021/12/31 追記。winapi crate でも impl-default feature を有効にすると`default()`で初期化できるとのことです。@yuma140902 さんに [winapi 版記事のコメント](https://qiita.com/benki/items/de2e104a5866fad0ebab#comment-52fef11d6500240076ba)で教えてもらいました。

```toml:Cargo.toml
[dependencies.winapi]
version = "0.3"
features = [
    "impl-default",
    "oaidl"
]
```
```rust:main.rs
use winapi::um::oaidl::VARIANT;

fn main() {
    let _variant = VARIANT::default();
}
```

## VARIANT 構造体について

今回のように COM を扱っていると`VARIANT`構造体が出てくるわけですが、windows crate では`VARIANT`構造体のメンバが`ManuallyDrop`で包まれています。つまりクライアント側でメモリ開放の面倒を見てやる必要があります。（なんでこんな実装になってるのかな？）とりあえず`VARIANT`構造体を自分で作った`Variant`構造体に捕捉しておいて、`Variant`構造体が`drop`するときにメンバの型によって`ManuallyDrop`なメンバを`drop`するようにしておきました。こんな実装で問題ない？

```rust:windowsの場合
struct Variant(VARIANT);
impl Drop for Variant {
    fn drop(&mut self) {
        unsafe {
            match VARENUM(self.0.Anonymous.Anonymous.vt as i32) {
                VT_BSTR => ManuallyDrop::drop(&mut (*self.0.Anonymous.Anonymous).Anonymous.bstrVal),
                VT_DISPATCH => ManuallyDrop::drop(&mut (*self.0.Anonymous.Anonymous).Anonymous.pdispVal),
                _ => (),
            }
            ManuallyDrop::drop(&mut self.0.Anonymous.Anonymous);
        }
    }
}

let mut variant = VARIANT::default();
(*variant.Anonymous.Anonymous).vt = VT_BSTR.0 as u16;
(*variant.Anonymous.Anonymous).Anonymous.bstrVal = ManuallyDrop::new("test".into());

// VARIANT を Variant 構造体に捕捉
let variant = Variant(variant);

// Variant が drop するときに ManuallyDrop が drop される
```

上記 2021/12/31 に訂正しました。当初`core::mem::drop`でリソース開放しているつもりでしたが、`core::mem::drop`に参照を渡しても意味ないですね…。`Manually::drop`を呼ぶべきでした。

## まとめ

大事なことはだいたい書いたのでサンプルコードをまとめてどうぞ。

<details><summary>コピペ用サンプルコード</summary><div>

```toml:Cargo.toml
[package]
name = "excel_to_pdf"
version = "0.1.0"
authors = ["benki"]
edition = "2021"

[dependencies]
anyhow = "1.0"

[dependencies.windows]
version = "0.39"
features = [
    "Win32_System_Com",
    "Win32_Foundation",
    "Win32_System_Ole"
]
```
```rust:main.rs
use anyhow::{Context, Result};
use std::mem::ManuallyDrop;
use std::path::Path;
use std::ptr;
use windows::{
    core::{GUID, HSTRING, PWSTR},
    w,
    Win32::System::{
        Com::{
            CLSIDFromProgID, CoCreateInstance, CoInitialize, CoUninitialize, IDispatch,
            CLSCTX_LOCAL_SERVER, DISPPARAMS, VARIANT,
        },
        Ole::{
            DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT, DISPID_PROPERTYPUT,
            VARENUM, VT_BOOL, VT_BSTR, VT_DISPATCH, VT_I4,
        },
    },
};

fn main() {
    match excel_to_pdf("path to excel")
    {
        Ok(_) => (),
        Err(e) => println!("{}", e),
    }
}

struct Com;
impl Drop for Com {
    fn drop(&mut self) {
        unsafe {
            CoUninitialize();
        }
    }
}

struct Variant(VARIANT);

impl Variant {
    fn dispval(&self) -> Option<&IDispatch> {
        unsafe { (*(*self.0.Anonymous.Anonymous).Anonymous.pdispVal).as_ref() }
    }
}

impl Drop for Variant {
    fn drop(&mut self) {
        unsafe {
            match VARENUM(self.0.Anonymous.Anonymous.vt as i32) {
                VT_BSTR => ManuallyDrop::drop(&mut (*self.0.Anonymous.Anonymous).Anonymous.bstrVal),
                VT_DISPATCH => {
                    ManuallyDrop::drop(&mut (*self.0.Anonymous.Anonymous).Anonymous.pdispVal)
                }
                _ => (),
            }
            ManuallyDrop::drop(&mut self.0.Anonymous.Anonymous);
        }
    }
}

fn excel_to_pdf<P: AsRef<Path>>(path: P) -> Result<()> {
    unsafe {
        // Initialize COM for this thread...
        CoInitialize(ptr::null())?;
        let _com = Com;

        // Get CLSID for our server...
        let clsid = CLSIDFromProgID(w!("Excel.Application"))?;

        // Start server and get IDispatch...
        let pxlapp: IDispatch = CoCreateInstance(&clsid, None, CLSCTX_LOCAL_SERVER)?;
        let pxlapp = Some(&pxlapp);

        // Make it visible (i.e. app.visible = 1)
        let visible = variant_int(0);
        auto_wrap(DISPATCH_PROPERTYPUT, pxlapp, w!("Visible"), vec![visible])?;

        let display_alerts = variant_int(0);
        auto_wrap(
            DISPATCH_PROPERTYPUT,
            pxlapp,
            w!("DisplayAlerts"),
            vec![display_alerts],
        )?;

        // Get Workbooks collection
        let result = auto_wrap(DISPATCH_PROPERTYGET, pxlapp, w!("Workbooks"), vec![])?;
        let pxlbooks = result.dispval();

        // Call Workbooks.Open() to get a new workbook...
        let param = variant_bstr(&path.as_ref().to_string_lossy());
        let result = auto_wrap(DISPATCH_PROPERTYGET, pxlbooks, w!("Open"), vec![param])?;
        let pxlbook = result.dispval();

        // Get ActiveSheet object
        let result = auto_wrap(DISPATCH_PROPERTYGET, pxlbook, w!("ActiveSheet"), vec![])?;
        let pxlsheet = result.dispval();

        // Call wb.ActiveSheet.ExportAsFixedFormat(xlTypePDF, "path to pdf")
        let typ = variant_int(0);
        let parent = path.as_ref().parent().context("no parent.")?;
        let file_stem = path.as_ref().file_stem().context("no file_stem.")?;
        let pdf_path = parent.join(format!("{}.pdf", file_stem.to_string_lossy()));
        let file_name = variant_bstr(pdf_path.to_string_lossy());

        auto_wrap(
            DISPATCH_METHOD,
            pxlsheet,
            w!("ExportAsFixedFormat"),
            vec![file_name, typ],
        )?;

        // wb.Close(False)
        let close = variant_bool(false);
        auto_wrap(DISPATCH_METHOD, pxlbook, w!("Close"), vec![close])?;

        // Tell Excel to quit (i.e. App.Quit)
        auto_wrap(DISPATCH_METHOD, pxlapp, w!("Quit"), vec![])?;

        // Automatically release references...

        // Automatically uninitialize COM for this thread...
    }
    Ok(())
}

// AutoWrap() - Automation helper function...
unsafe fn auto_wrap(
    auto_type: u32,
    pdisp: Option<&IDispatch>,
    ptname: &HSTRING,
    args: Vec<Variant>,
) -> Result<Variant> {
    let pdisp = pdisp.context("null IDispatch passed to auto_wrap.")?;

    // Variables used...
    let mut dp = DISPPARAMS::default();
    let mut dispid_named = DISPID_PROPERTYPUT;
    let mut dispid = 0;

    // Get DISPID for name passed...
    pdisp.GetIDsOfNames(
        &GUID::default(),
        &PWSTR(ptname.as_ptr() as *mut _) as *const _,
        1,
        0x0400, // LOCALE_USER_DEFAULT, 1024u32
        &mut dispid,
    )?;

    // Build DISPPARAMS
    dp.cArgs = args.len() as u32;
    dp.rgvarg = args.as_ptr() as *const VARIANT as *mut _;

    // Handle special-case for property-puts!
    if auto_type & DISPATCH_PROPERTYPUT > 0 {
        dp.cNamedArgs = 1;
        dp.rgdispidNamedArgs = &mut dispid_named;
    }

    let mut vresult = VARIANT::default();

    // Make the call!
    pdisp.Invoke(
        dispid,
        &GUID::default(),
        0x0800, // LOCALE_SYSTEM_DEFAULT, 2048u32
        auto_type as u16,
        &dp,
        &mut vresult,
        ptr::null_mut(),
        ptr::null_mut(),
    )?;

    Ok(Variant(vresult))
}

unsafe fn variant_int(value: i32) -> Variant {
    let mut variant = VARIANT::default();
    (*variant.Anonymous.Anonymous).vt = VT_I4.0 as u16;
    (*variant.Anonymous.Anonymous).Anonymous.lVal = value;
    Variant(variant)
}

unsafe fn variant_bstr<S: AsRef<str>>(value: S) -> Variant {
    let mut variant = VARIANT::default();
    (*variant.Anonymous.Anonymous).vt = VT_BSTR.0 as u16;
    // impl From<&str> for BSTR が実装されているので into() 呼ぶだけ！
    // impl Drop for BSTR も実装されているので、SysFreeString しなくても良い！
    (*variant.Anonymous.Anonymous).Anonymous.bstrVal = ManuallyDrop::new(value.as_ref().into());
    Variant(variant)
}

unsafe fn variant_bool(value: bool) -> Variant {
    let mut variant = VARIANT::default();
    (*variant.Anonymous.Anonymous).vt = VT_BOOL.0 as u16;
    (*variant.Anonymous.Anonymous).Anonymous.boolVal = if value { 1 } else { 0 };
    Variant(variant)
}

```
</div></details>

winapi crate から windows crate に移行してみました。windows crate の方が Rust っぽく書けるので良いと思います:relaxed: