LoginSignup
10
12

More than 1 year has passed since last update.

Rust で Excel オートメーション

Last updated at Posted at 2020-09-12

2021/12/29 追記

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

動機

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

環境

Windows10 64bit
Excel 2010
rustc 1.46.0
winapi 0.3.9

VBScript

↓こういうこと(Excel を PDF に変換)がしたい。Rust で。

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 という Microsoft 公式のドキュメントを愚直に Rust に写経していくだけの簡単な作業です。そんなわけで unsafe なコードをゴリゴリ書いていくので Rust を使う理由はないです。ただこういうコードを書くと参照とポインタの関係とか文字コードのこととか色々な知識が蓄積されていくので、これはこれで良いのではないかなと思います。

Win32 API

Rust で Win32 API 使うとなると winapi crate です。あと Microsoft の公式ドキュメントが欠かせませんね。

Rust から呼びたい Win32 API があったら winapi crate のドキュメントで検索してどのモジュールに含まれているかを確認。Cargo.tomlfeatures にモジュール名を追加して、main.rsuse winapi::um::objbase::CoInitializeと書いてインポートしていきます。

Cargo.toml
[dependencies.winapi]
version = "0.3"
features = ["combaseapi", "objbase"]
main.rs
use winapi::{
    um::{
        combaseapi:CoUninitialize,
        objbase::CoInitialize
    }
};

CoInitialize

まずCoInitialize関数で COM ライブラリを初期化する必要があるらしいです。お約束みたいなものだと考えておきます。新しいアプリは CoInitializeEx を使えと書いてありますが愚直に写経するので今回無視してCoInitializeを使います。あとドキュメントに初期化したら必ず CoUninitialize を呼べと書いてあるのでimpl Dropを使うことにしました。以下、CRustを並べて書いていきます。

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

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

// Uninitialize COM for this thread...
CoUninitialize();
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

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

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
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

続いてCoCreateInstanceでなにやらオブジェクトを作成するようです。Rust に写経するときに困るのが、たまに winapi crate で定義されていない定数があったり、ポインタ周りの扱いです。

IID_IDispatchは Microsoft の公式ドキュメントによるとこんな値です。winapi crate ではこの値が定義されていないようなので、自分で書きます。

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

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
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

最後にVARIANT構造体の説明です。Microsoft の公式ドキュメントを見ての通りunionにたくさんの型がありますね。vtでデータの型を指定して、実際の値はunionに保存するようです。Rust の winapi crate ではちょっと冗長な書き方になります。

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
// 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(),
)?;

まとめ

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

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

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

10
12
1

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
10
12