Rust で Excel オートメーション

2021/12/29 追記

windows-rs 版の記事も書きました。windows-rs を使うときは参考にしてください。

https://qiita.com/benki/items/42099c58e07b16293609

## 動機
Excel 便利ですよね。うちの会社も例に漏れず Excel であらゆる文書を作成しています。ただ客先とかメーカに文書を配布するときは PDF で送付なんてことがよくあるので、大量の Excel を PDF に変換するということが発生します。お手軽にその作業をやるなら VBScript とか Python を使うのが一般的？と思いますが、あえて Rust でやってみたいと思います。

## 環境
Windows10 64bit
Excel 2010
rustc 1.46.0
[winapi](https://crates.io/crates/winapi) 0.3.9

## VBScript
↓こういうこと（Excel を PDF に変換）がしたい。Rust で。

```vb:excel_to_pdf.vbs
Set oXlsApp = CreateObject("Excel.Application")
If oXlsApp is Nothing Then
    WScript.Quit
End If

oXlsApp.Application.Visible = False
oXlsApp.Application.DisplayAlerts = False
Set wb = oXlsApp.Application.Workbooks.Open("path to xls")
Call wb.ActiveSheet.ExportAsFixedFormat(xlTypePDF, "path to pdf")
wb.Close(False)
oXlsApp.Quit
Set oXlsApp = Nothing
```

## 実装
実際には [How to automate Excel from C++ without using MFC or #import](https://docs.microsoft.com/en-us/previous-versions/office/troubleshoot/office-developer/automate-excel-from-c) という Microsoft 公式のドキュメントを愚直に Rust に写経していくだけの簡単な作業です。そんなわけで unsafe なコードをゴリゴリ書いていくので Rust を使う理由はないです。ただこういうコードを書くと参照とポインタの関係とか文字コードのこととか色々な知識が蓄積されていくので、これはこれで良いのではないかなと思います。

### Win32 API
Rust で Win32 API 使うとなると [winapi](https://crates.io/crates/winapi) crate です。あと Microsoft の[公式ドキュメント](https://docs.microsoft.com/en-us/windows/win32/api/)が欠かせませんね。

Rust から呼びたい Win32 API があったら [winapi crate のドキュメントで検索](https://docs.rs/winapi/0.3.9/winapi/?search=CoInitialize)してどのモジュールに含まれているかを確認。```Cargo.toml``` の ```features``` にモジュール名を追加して、```main.rs```に```use winapi::um::objbase::CoInitialize```と書いてインポートしていきます。

```toml:Cargo.toml
[dependencies.winapi]
version = "0.3"
features = ["combaseapi", "objbase"]
```
```rust:main.rs
use winapi::{
    um::{
        combaseapi:CoUninitialize,
        objbase::CoInitialize
    }
};
```
### [CoInitialize](https://docs.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitialize) 
まず```CoInitialize```関数で COM ライブラリを初期化する必要があるらしいです。お約束みたいなものだと考えておきます。新しいアプリは [CoInitializeEx](https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-couninitialize) を使えと書いてありますが愚直に写経するので今回無視して```CoInitialize```を使います。あとドキュメントに初期化したら必ず [CoUninitialize](https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-couninitialize) を呼べと書いてあるので```impl Drop```を使うことにしました。以下、```C```と```Rust```を並べて書いていきます。

```cpp:C
// Initialize COM for this thread...
CoInitialize(NULL);

// いろいろな処理
// 途中でリターンして CoUninitialize を呼び忘れることも...

// Uninitialize COM for this thread...
CoUninitialize();
```
```rust:Rust
struct Com;
impl Drop for Com {
    fn drop(&mut self) {
        unsafe {
            CoUninitialize();
        }
    }
}

unsafe {
    // Initialize COM for this thread...
    CoInitialize(ptr::null_mut());
    let _com = Com;
    // いろいろな処理
    // スコープを抜けるときに drop が自動的に呼ばれる
    // Uninitialize COM for this thread...
}
```

こうしておけば終了処理を忘れることもないですね！

### [CLSIDFromProgID](https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-clsidfromprogid)
次に```CLSIDFromProgID``` API で CLSID を取得します。この関数の第1引数は```LPCOLESTR```という見たこともない型ですが、UTF-16 です。第2引数は```CLSID```構造体へのポインタです。C は```CLSID clsid;```という記述で構造体のメモリ領域を確保できますが、Rust ではメンバもすべて書く必要があります。それはさすがに面倒なので```std::mem::MaybeUninit```で確保します。エラーの場合の処置は [anyhow](https://crates.io/crates/anyhow) crate の```ensure!```マクロが便利だと思います。

```c:C
// Get CLSID for our server...
CLSID clsid;
HRESULT hr = CLSIDFromProgID(L"Excel.Application", &clsid);

if(FAILED(hr)) {
    ::MessageBox(NULL, "CLSIDFromProgID() failed", "Error", 0x10010);
    return -1;
}
```
```rust:Rust
use anyhow::ensure;

// Get CLSID for our server...
let mut clsid = init::<CLSID>();
let id = l("Excel.Application");
let hr = CLSIDFromProgID(id.as_ptr(), &mut clsid);
ensure!(SUCCEEDED(hr), "CLSIDFromProgID failed");

fn l(source: &str) -> Vec<u16> {
    source.encode_utf16().chain(Some(0)).collect()
}
unsafe fn init<T>() -> T {
    mem::MaybeUninit::<T>::uninit().assume_init()
}
```

### [CoCreateInstance](https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance)
続いて```CoCreateInstance```でなにやらオブジェクトを作成するようです。Rust に写経するときに困るのが、たまに winapi crate で定義されていない定数があったり、ポインタ周りの扱いです。

```IID_IDispatch```は Microsoft の公式ドキュメントによると[こんな値](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/58504586-e4af-44a3-be04-f1dc281b7429)です。winapi crate ではこの値が定義されていないようなので、自分で書きます。

```CoCreateInstance```関数の最後の引数は```IDispatch```へのポインタを void ポインタにキャストしていますね。Rust の場合は```&mut pxlapp as *mut *mut IDispatch as *mut *mut c_void```にように書く必要があります。

```c:C
// Start server and get IDispatch...
IDispatch *pXlApp;
hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_IDispatch, (void **)&pXlApp);
if(FAILED(hr)) {
    ::MessageBox(NULL, "Excel not registered properly", "Error", 0x10010);
    return -2;
}
```
```rust:Rust
use winapi::shared::guiddef::GUID;

const IID_IDISPATCH: GUID = GUID {
    Data1: 0x00020400,
    Data2: 0x0,
    Data3: 0x0,
    Data4: [0xC0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46],
};

// Start server and get IDispatch...
let mut pxlapp = ptr::null_mut::<IDispatch>();
let hr = CoCreateInstance(
    &clsid,
    ptr::null_mut(),
    CLSCTX_LOCAL_SERVER,
    &IID_IDISPATCH,
    &mut pxlapp as *mut *mut IDispatch as *mut *mut c_void,
);
ensure!(SUCCEEDED(hr), "Excel not registered properly");
```

### [VARIANT](https://docs.microsoft.com/en-us/windows/win32/api/oaidl/ns-oaidl-variant)
最後に```VARIANT```構造体の説明です。Microsoft の公式ドキュメントを見ての通り```union```にたくさんの型がありますね。```vt```でデータの型を指定して、実際の値は```union```に保存するようです。Rust の winapi crate ではちょっと冗長な書き方になります。

```c:C
// Make it visible (i.e. app.visible = 1)
VARIANT x;
x.vt = VT_I4;
x.lVal = 1;
AutoWrap(DISPATCH_PROPERTYPUT, NULL, pXlApp, L"Visible", 1, x);
```
```rust:Rust
// Make it visible (i.e. app.visible = 1)
let mut x = init::<VARIANT>();
x.n1.n2_mut().vt = VT_I4 as u16;
*x.n1.n2_mut().n3.lVal_mut() = 0; //Excel のウィンドウを非表示にするなら "0"
auto_wrap(
    DISPATCH_PROPERTYPUT,
    ptr::null_mut(),
    pxlapp,
    "Visible",
    vec![x].as_mut_slice(),
)?;
```

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

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

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

[dependencies]
anyhow = "1.0"

[dependencies.winapi]
version = "0.3"
features = ["combaseapi", "objbase", "winerror", "guiddef", "oaidl", "wtypesbase", "ntdef", "wtypes", "oleauto"]
```
```rust:main.rs
use anyhow::{anyhow, ensure, Result};
use std::mem;
use std::path::Path;
use std::ptr;
use winapi::{
    ctypes::c_void,
    shared::{
        guiddef::{CLSID, GUID, IID_NULL},
        ntdef::{LOCALE_SYSTEM_DEFAULT, LOCALE_USER_DEFAULT},
        winerror::SUCCEEDED,
        wtypes::{VT_BOOL, VT_BSTR, VT_I4, VT_INT},
        wtypesbase::CLSCTX_LOCAL_SERVER,
    },
    um::{
        combaseapi::{CLSIDFromProgID, CoCreateInstance, CoUninitialize},
        oaidl::{IDispatch, DISPID, DISPID_PROPERTYPUT, DISPPARAMS, VARIANT},
        objbase::CoInitialize,
        oleauto::{
            SysAllocString, SysFreeString, VariantInit, DISPATCH_METHOD, DISPATCH_PROPERTYGET,
            DISPATCH_PROPERTYPUT,
        },
    },
};
const IID_IDISPATCH: GUID = GUID {
    Data1: 0x00020400,
    Data2: 0x0,
    Data3: 0x0,
    Data4: [0xC0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46],
};
const XLTYPEPDF: i32 = 0;

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

fn main() -> Result<()> {
    excel_to_pdf("path to xls")?;
    Ok(())
}

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

        // Get CLSID for our server...
        let mut clsid = init::<CLSID>();
        let id = l("Excel.Application");
        let hr = CLSIDFromProgID(id.as_ptr(), &mut clsid);
        ensure!(SUCCEEDED(hr), "CLSIDFromProgID failed");

        // Start server and get IDispatch...
        let mut pxlapp = ptr::null_mut::<IDispatch>();
        let hr = CoCreateInstance(
            &clsid,
            ptr::null_mut(),
            CLSCTX_LOCAL_SERVER,
            &IID_IDISPATCH,
            &mut pxlapp as *mut *mut IDispatch as *mut *mut c_void,
        );
        ensure!(SUCCEEDED(hr), "Excel not registered properly");

        // Make it visible (i.e. app.visible = 1)
        let mut visible = init::<VARIANT>();
        visible.n1.n2_mut().vt = VT_I4 as u16;
        *visible.n1.n2_mut().n3.lVal_mut() = 0;
        auto_wrap(
            DISPATCH_PROPERTYPUT,
            ptr::null_mut(),
            pxlapp,
            "Visible",
            vec![visible].as_mut_slice(),
        )?;

        let mut display_alerts = init::<VARIANT>();
        display_alerts.n1.n2_mut().vt = VT_I4 as u16;
        *display_alerts.n1.n2_mut().n3.lVal_mut() = 0;
        auto_wrap(
            DISPATCH_PROPERTYPUT,
            ptr::null_mut(),
            pxlapp,
            "DisplayAlerts",
            vec![display_alerts].as_mut_slice(),
        )?;

        // Get Workbooks collection
        let mut result = init::<VARIANT>();
        VariantInit(&mut result);
        auto_wrap(
            DISPATCH_PROPERTYGET,
            &mut result,
            pxlapp,
            "Workbooks",
            vec![].as_mut_slice(),
        )?;
        let pxlbooks = *result.n1.n2().n3.pdispVal();
        ensure!(!pxlbooks.is_null(), "Workbooks failed");

        let path_ = l(path.as_ref().to_string_lossy().as_ref());
        let mut param = init::<VARIANT>();
        param.n1.n2_mut().vt = VT_BSTR as u16;
        *param.n1.n2_mut().n3.bstrVal_mut() = SysAllocString(path_.as_ptr());

        // Call Workbooks.Open() to get a new workbook...
        let mut result = init::<VARIANT>();
        VariantInit(&mut result);
        auto_wrap(
            DISPATCH_PROPERTYGET,
            &mut result,
            pxlbooks,
            "Open",
            vec![param].as_mut_slice(),
        )?;
        let pxlbook = *result.n1.n2().n3.pdispVal();
        SysFreeString(*param.n1.n2().n3.bstrVal());

        // Get ActiveSheet object
        let mut result = init::<VARIANT>();
        VariantInit(&mut result);
        auto_wrap(
            DISPATCH_PROPERTYGET,
            &mut result,
            pxlbook,
            "ActiveSheet",
            vec![].as_mut_slice(),
        )?;
        let pxlsheet = *result.n1.n2().n3.pdispVal();
        ensure!(!pxlsheet.is_null(), "Open workbook failed. {}", path.as_ref().to_string_lossy());

        // Call wb.ActiveSheet.ExportAsFixedFormat(xlTypePDF, "path to pdf")
        let mut typ = init::<VARIANT>();
        typ.n1.n2_mut().vt = VT_INT as u16;
        *typ.n1.n2_mut().n3.intVal_mut() = XLTYPEPDF;

        let parent = path.as_ref().parent().ok_or(anyhow!("Cannot get parent directory"))?;
        let file_stem = path.as_ref().file_stem().ok_or(anyhow!("Cannot get file_stem"))?.to_string_lossy();
        let pdf_file_name = format!("{}.pdf", file_stem);
        let pdf_path = parent.join(pdf_file_name);
        let pdf_path_ = l(pdf_path.to_string_lossy().as_ref());
        let mut file_name = init::<VARIANT>();
        file_name.n1.n2_mut().vt = VT_BSTR as u16;
        *file_name.n1.n2_mut().n3.bstrVal_mut() = SysAllocString(pdf_path_.as_ptr());

        auto_wrap(
            DISPATCH_METHOD,
            ptr::null_mut(),
            pxlsheet,
            "ExportAsFixedFormat",
            vec![file_name, typ].as_mut_slice(), // 引数の順番が逆になります！
        )?;
        SysFreeString(*file_name.n1.n2().n3.bstrVal());

        // wb.Close(False)
        let mut close = init::<VARIANT>();
        close.n1.n2_mut().vt = VT_BOOL as u16;
        *close.n1.n2_mut().n3.boolVal_mut() = 0;
        auto_wrap(
            DISPATCH_METHOD,
            ptr::null_mut(),
            pxlbook,
            "Close",
            vec![close].as_mut_slice(),
        )?;

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

        // Release references...
        (*pxlsheet).Release();
        (*pxlbook).Release();
        (*pxlbooks).Release();
        (*pxlapp).Release();

        // Uninitialize COM for this thread...
    }
    Ok(())
}

// AutoWrap() - Automation helper function...
unsafe fn auto_wrap(
    auto_type: u16,
    pvresult: *mut VARIANT,
    pdisp: *mut IDispatch,
    ptname: &str,
    args: &mut [VARIANT],
) -> Result<()> {
    ensure!(
        !(pdisp as *mut IDispatch).is_null(),
        "null IDispatch passed to auto_wrap"
    );

    // Variables used...
    let mut dp = DISPPARAMS {
        rgvarg: ptr::null_mut(),
        rgdispidNamedArgs: ptr::null_mut(),
        cArgs: 0,
        cNamedArgs: 0,
    };
    let mut dispid_named = DISPID_PROPERTYPUT;
    let mut dispid: DISPID = 0;
    let mut ptname_ = l(ptname);

    // Get DISPID for name passed...
    let hr = (*pdisp).GetIDsOfNames(
        &IID_NULL,
        &mut ptname_.as_mut_ptr(),
        1,
        LOCALE_USER_DEFAULT,
        &mut dispid,
    );
    ensure!(SUCCEEDED(hr), "pdisp.GetIDsOfNames({}) failed", ptname);

    // Build DISPPARAMS
    dp.cArgs = args.len() as u32;
    dp.rgvarg = args.as_mut_ptr();

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

    // Make the call!
    let hr = (*pdisp).Invoke(
        dispid,
        &IID_NULL,
        LOCALE_SYSTEM_DEFAULT,
        auto_type as u16,
        &mut dp,
        pvresult,
        ptr::null_mut(),
        ptr::null_mut(),
    );
    ensure!(SUCCEEDED(hr), "IDispatch::Invoke({}) failed", ptname);
    Ok(())
}

fn l(source: &str) -> Vec<u16> {
    source.encode_utf16().chain(Some(0)).collect()
}

unsafe fn init<T>() -> T {
    mem::MaybeUninit::<T>::uninit().assume_init()
}
```
</div></details>

これで Rust から Excel をゴリゴリいじれるようになりました。やったぜ:relaxed: