Reputation: 131
We're using a Brother label printer (QL-series) via its programming interface/SDK called 'bPac'. The tool, of which the label printing facility is part, is currently being rewritten from Ruby to Rust. During this process, I got stuck on the Win32/COM/OLE thing in Rust. A minimal working example in Ruby would be simple enough:
doc = WIN32OLE.new "bpac.Document"
doc.open 'some_label.lbx'
doc.SetPrinter "Brother QL-810W", true
print_opts = 0
doc.StartPrint("", print_opts)
doc.PrintOut(1, print_opts)
doc.EndPrint
I'd like to have a similar simple working example in Rust to start off with. As I'm not familiar with the Windows API the windows-rs
crate is quite overwhelming. I figured, that I probably need the System::Com
part from it. Here's what I started off with:
use windows::Win32::System::{Com, Ole};
use ::windows::core::Result;
pub fn print() {
unsafe { Com::CoInitializeEx(std::ptr::null(), Com::COINIT_APARTMENTTHREADED) }.unwrap();
let clsid = unsafe { Com::CLSIDFromProgID("bpac.Document") };
println!("We've got a CLSID: {:?}", clsid);
let obj: Result<Com::IDispatch> = unsafe { Com::CoCreateInstance(&clsid.unwrap(), None, Com::CLSCTX_ALL) };
println!("IDispatch: {:?}", obj);
}
This way I can acquire an IDispatch
object, which I should be able to query for available methods and properties. Having trouble calling into this low-level (very close to the C-metal) API. I found win-idispatch crate, but that does not seem to play ball with windows-rs
... :-/
Upvotes: 3
Views: 2990
Reputation: 153
I wanted to do something similar: COM Office/Excel Automation using Rust.
In a nutshell I've built a wrapper around IDispatch::GetIDsOfNames()
and IDispatch::Invoke()
and for arguments one would use VARIANT
.
The following resources helped me build a solution:
https://stuncloud.wordpress.com/2021/08/17/rust_com/
https://qiita.com/benki/items/42099c58e07b16293609
https://learn.microsoft.com/en-us/previous-versions/office/troubleshoot/office-developer/automate-excel-from-c
Edit 2024-03-19: Add a quick-and-dirty example:
Cargo.toml
[package]
name = "office_interop"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.81"
[dependencies.windows]
version = "0.54.0"
features = [
"Win32_Foundation",
"Win32_System_Com",
"Win32_System_Ole",
]
main.rs
use anyhow::{anyhow, Context};
use std::time::Duration;
use std::{env, thread};
use windows::core::*;
use windows::Win32::System::Com::*;
use windows::Win32::System::Ole::*;
const LOCALE_USER_DEFAULT: u32 = 0x0400;
const LOCALE_SYSTEM_DEFAULT: u32 = 0x0800;
//cargo run -- FILE
fn main() -> anyhow::Result<()> {
let mut args = env::args();
let _ = args.next();
let file = args.next();
let file = file.ok_or(anyhow!("arg1 missing"))?;
unsafe {
let res = CoInitialize(None);
if res.is_err() {
return Err(anyhow!("error: {}", res.message()));
}
let _com = DeferCoUninitialize;
let clsid = CLSIDFromProgID(PCWSTR::from_raw(
HSTRING::from("Excel.Application").as_ptr(),
))
.with_context(|| "CLSIDFromProgID")?;
println!("{:?}", clsid);
let excel = CoCreateInstance(&clsid, None, CLSCTX_LOCAL_SERVER)
.with_context(|| "CoCreateInstance")?;
let excel = IDispatchWrapper(excel);
let _excel = DeferExcelQuit(&excel);
excel
.put("Visible", vec![false.into()])
.with_context(|| "Visible false")?;
excel
.put("DisplayAlerts", vec![false.into()])
.with_context(|| "DisplayAlerts false")?;
let result = excel.get("Workbooks").with_context(|| "get Workbooks")?;
let workbooks = result.idispatch().with_context(|| "idispatch Workbooks")?;
let result = workbooks
.call("Open", vec![(&file).into()])
.with_context(|| format!("Failed to open file \"{}\"!", file))?;
let workbook = result.idispatch().with_context(|| "idispatch Workbook")?;
thread::sleep(Duration::from_millis(3000));
let result = excel.get("Caption").with_context(|| "get Caption")?;
println!("{}", result.string().with_context(|| "Caption string")?);
workbook
.call("Close", vec![false.into()])
.with_context(|| "call Close")?;
thread::sleep(Duration::from_millis(3000));
let result = excel.get("Caption").with_context(|| "get Caption")?;
println!("{}", result.string().with_context(|| "Caption string")?);
thread::sleep(Duration::from_millis(3000));
Ok(())
}
}
pub struct Variant(VARIANT);
impl From<bool> for Variant {
fn from(value: bool) -> Self {
Self(value.into())
}
}
impl From<i32> for Variant {
fn from(value: i32) -> Self {
Self(value.into())
}
}
impl From<&str> for Variant {
fn from(value: &str) -> Self {
Self(BSTR::from(value).into())
}
}
impl From<&String> for Variant {
fn from(value: &String) -> Self {
Self(BSTR::from(value).into())
}
}
impl Variant {
pub fn bool(&self) -> anyhow::Result<bool> {
Ok(bool::try_from(&self.0)?)
}
pub fn int(&self) -> anyhow::Result<i32> {
Ok(i32::try_from(&self.0)?)
}
pub fn string(&self) -> anyhow::Result<String> {
Ok(BSTR::try_from(&self.0)?.to_string())
}
pub fn idispatch(&self) -> anyhow::Result<IDispatchWrapper> {
Ok(IDispatchWrapper(IDispatch::try_from(&self.0)?))
}
pub fn vt(&self) -> u16 {
unsafe { self.0.as_raw().Anonymous.Anonymous.vt }
}
}
pub struct IDispatchWrapper(pub IDispatch);
impl IDispatchWrapper {
pub fn invoke(
&self,
flags: DISPATCH_FLAGS,
name: &str,
mut args: Vec<Variant>,
) -> anyhow::Result<Variant> {
unsafe {
let mut dispid = 0;
self.0
.GetIDsOfNames(
&GUID::default(),
&PCWSTR::from_raw(HSTRING::from(name).as_ptr()),
1,
LOCALE_USER_DEFAULT,
&mut dispid,
)
.with_context(|| "GetIDsOfNames")?;
let mut dp = DISPPARAMS::default();
let mut dispid_named = DISPID_PROPERTYPUT;
if !args.is_empty() {
args.reverse();
dp.cArgs = args.len() as u32;
dp.rgvarg = args.as_mut_ptr() as *mut VARIANT;
// Handle special-case for property-puts!
if (flags & DISPATCH_PROPERTYPUT) != DISPATCH_FLAGS(0) {
dp.cNamedArgs = 1;
dp.rgdispidNamedArgs = &mut dispid_named;
}
}
let mut result = VARIANT::default();
self.0
.Invoke(
dispid,
&GUID::default(),
LOCALE_SYSTEM_DEFAULT,
flags,
&dp,
Some(&mut result),
None,
None,
)
.with_context(|| "Invoke")?;
Ok(Variant(result))
}
}
pub fn get(&self, name: &str) -> anyhow::Result<Variant> {
self.invoke(DISPATCH_PROPERTYGET, name, vec![])
}
pub fn int(&self, name: &str) -> anyhow::Result<i32> {
let result = self.get(name)?;
result.int()
}
pub fn bool(&self, name: &str) -> anyhow::Result<bool> {
let result = self.get(name)?;
result.bool()
}
pub fn string(&self, name: &str) -> anyhow::Result<String> {
let result = self.get(name)?;
result.string()
}
pub fn put(&self, name: &str, args: Vec<Variant>) -> anyhow::Result<Variant> {
self.invoke(DISPATCH_PROPERTYPUT, name, args)
}
pub fn call(&self, name: &str, args: Vec<Variant>) -> anyhow::Result<Variant> {
self.invoke(DISPATCH_METHOD, name, args)
}
}
pub struct DeferExcelQuit<'a>(pub &'a IDispatchWrapper);
impl Drop for DeferExcelQuit<'_> {
fn drop(&mut self) {
let result = self.0.get("Workbooks").unwrap();
let workbooks = result.idispatch().unwrap();
let count = workbooks.int("Count").unwrap();
for i in 1..=count {
let result = workbooks
.invoke(DISPATCH_PROPERTYGET, "Item", vec![i.into()])
.unwrap();
let workbook = result.idispatch().unwrap();
workbook.put("Saved", vec![true.into()]).unwrap();
}
self.0.call("Quit", vec![]).unwrap(); //todo: quit doesn't kill process after open and Visible=true
}
}
pub struct DeferCoUninitialize;
impl Drop for DeferCoUninitialize {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
Upvotes: 6