LoginSignup
41
28

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-29

動機

下記の記事では winapi を使っていました。

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

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

環境

Windows 10 64bit
Excel 2016
rustc 1.63.0
windows 0.39.0

windows crate と 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 を使うべきでしょう。

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

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(..);
}
windowsの場合
// 0.39.0。Rust の世界の文字列を w! マクロで変換する。
let clsid = CLSIDFromProgID(w!("Excel.Application"))?;

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

Cargo.toml
[dependencies]
anyhow = "1.0"

[dependencies.windows]
version = "0.39"
features = [
    "Win32_System_Com",
    "Win32_Foundation",
]
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();って書くだけで良いです。いいね!

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

fn init::<T>() -> T {
    std::mem::MaybeUninit::<T>::uninit().assume_init()
}
windowsの場合
let variant = VARIANT::default();

2021/12/31 追記。winapi crate でも impl-default feature を有効にするとdefault()で初期化できるとのことです。@yuma140902 さんに winapi 版記事のコメントで教えてもらいました。

Cargo.toml
[dependencies.winapi]
version = "0.3"
features = [
    "impl-default",
    "oaidl"
]
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するようにしておきました。こんな実装で問題ない?

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を呼ぶべきでした。

まとめ

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

コピペ用サンプルコード
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"
]
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)
}

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

41
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
28