app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
//! Late-bound COM plumbing: VARIANTs and IDispatch calls, Windows only.
//!
//! SOLIDWORKS ships a type library, but a bridge that early-binds it needs the
//! interop assemblies of the exact installed version. Late binding
//! (`GetIDsOfNames` + `Invoke`) is what the PowerShell sidecar did and what
//! every SOLIDWORKS macro does — it works against whatever version is on the
//! machine, which is the only thing a bridge can promise.
//!
//! Everything here is `unsafe` at the edges and small on purpose: a [`Var`]
//! owns its VARIANT and clears it on drop; [`Disp`] turns a name plus arguments
//! into an `Invoke`, and turns an HRESULT plus EXCEPINFO into an error message
//! a human can act on.
use std::ffi::c_void;
use windows::core::{IUnknown, Interface, BSTR, GUID, PCWSTR};
use windows::Win32::System::Com::{
IDispatch, DISPATCH_FLAGS, DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT,
DISPPARAMS, EXCEPINFO, SAFEARRAY,
};
use windows::Win32::System::Ole::{
SafeArrayCreateVector, SafeArrayGetElement, SafeArrayGetLBound, SafeArrayGetUBound,
SafeArrayGetVartype, SafeArrayPutElement, DISPID_PROPERTYPUT,
};
use windows::Win32::System::Variant::{
VariantChangeType, VariantClear, VariantCopy, VARENUM, VARIANT, VT_ARRAY, VT_BOOL, VT_BSTR,
VT_BYREF, VT_DISPATCH, VT_EMPTY, VT_I2, VT_I4, VT_NULL, VT_R8, VT_UI1, VT_UNKNOWN, VT_VARIANT,
};
use super::host::HostError;
/// Method-or-property-get: let the callee decide which it is. This is what
/// VBScript and the SOLIDWORKS macro recorder do, and it saves the bridge from
/// having to know whether `ActiveDoc` is a property and `GetTitle` a method.
pub const CALL: DISPATCH_FLAGS = DISPATCH_FLAGS(DISPATCH_METHOD.0 | DISPATCH_PROPERTYGET.0);
/// An owned VARIANT. Clears itself, so a BSTR or interface pointer coming back
/// from SOLIDWORKS is released exactly once.
pub struct Var(pub VARIANT);
impl Drop for Var {
fn drop(&mut self) {
unsafe {
let _ = VariantClear(&mut self.0);
}
}
}
unsafe fn build(vt: VARENUM, fill: impl FnOnce(&mut VARIANT)) -> Var {
let mut v = VARIANT::default();
(*v.Anonymous.Anonymous).vt = vt;
fill(&mut v);
Var(v)
}
impl Var {
pub fn empty() -> Var {
unsafe { build(VT_EMPTY, |_| {}) }
}
pub fn i4(n: i32) -> Var {
unsafe { build(VT_I4, |v| (*v.Anonymous.Anonymous).Anonymous.lVal = n) }
}
/// A `short` (VT_I2) — ISketchManager::CreateArc's Direction is one.
pub fn i2(n: i16) -> Var {
unsafe { build(VT_I2, |v| (*v.Anonymous.Anonymous).Anonymous.iVal = n) }
}
pub fn r8(n: f64) -> Var {
unsafe { build(VT_R8, |v| (*v.Anonymous.Anonymous).Anonymous.dblVal = n) }
}
pub fn boolean(b: bool) -> Var {
unsafe {
build(VT_BOOL, |v| {
// VARIANT_BOOL: -1 is TRUE, 0 is FALSE. Not 1.
(*v.Anonymous.Anonymous).Anonymous.boolVal =
windows::Win32::Foundation::VARIANT_BOOL(if b { -1 } else { 0 });
})
}
}
pub fn bstr(s: &str) -> Var {
unsafe {
build(VT_BSTR, |v| {
(*v.Anonymous.Anonymous).Anonymous.bstrVal =
std::mem::ManuallyDrop::new(BSTR::from(s));
})
}
}
/// **"NOTHING", for an `[in]` OBJECT PARAMETER.**
///
/// A `VT_DISPATCH` variant whose `pdispVal` is NULL — which is what a
/// late-bound caller passes where VBA writes `Nothing`. It is NOT the same as
/// `VT_EMPTY`, and this bridge believed it was from v0.2.0 until 2026-09-17.
///
/// MEASURED THAT DAY, on the live install: every STEP import failed, and with
/// the structured COM failure finally reaching the caller (0.7.8) it read
///
/// ```text
/// hostError.loadFile4.detail = "Type mismatch. (0x80020005) (argument index 1)"
/// ```
///
/// `rgvarg` is packed REVERSED (see `call_raw`), so `puArgErr = 1` is the
/// THIRD parameter of `LoadFile4(path, "r", importData, byref errors)` —
/// `importData` — which was being passed as `VT_EMPTY`. SOLIDWORKS wants an
/// object there and will take a null one; it will not take "no value at all".
///
/// The consequence worth recording: **`LoadFile4` had probably never worked
/// live.** The imports of 09-05 to 09-11 went through the `LoadFile2`
/// fallback, and the reason that route looked like the normal one was that
/// nothing ever showed the caller why the first had failed.
pub fn null_dispatch() -> Var {
unsafe {
build(VT_DISPATCH, |v| {
(*v.Anonymous.Anonymous).Anonymous.pdispVal = std::mem::ManuallyDrop::new(None);
})
}
}
pub fn dispatch(d: &IDispatch) -> Var {
unsafe {
build(VT_DISPATCH, |v| {
(*v.Anonymous.Anonymous).Anonymous.pdispVal =
std::mem::ManuallyDrop::new(Some(d.clone()));
})
}
}
/// A SAFEARRAY of doubles — what `IMathUtility::CreateTransform` wants, and
/// the only array this bridge ever BUILDS (as opposed to reads). Sixteen
/// doubles is a component transform; nothing else here needs one.
pub fn r8_array(values: &[f64]) -> Var {
unsafe {
let psa = SafeArrayCreateVector(VT_R8, 0, values.len() as u32);
if psa.is_null() {
return Var::empty();
}
for (i, v) in values.iter().enumerate() {
let idx = [i as i32];
let _ = SafeArrayPutElement(
psa,
idx.as_ptr(),
v as *const f64 as *const std::ffi::c_void,
);
}
build(VARENUM(VT_ARRAY.0 | VT_R8.0), |v| {
(*v.Anonymous.Anonymous).Anonymous.parray = psa;
})
}
}
/// A SAFEARRAY of VARIANTs, each holding one object — the shape a VBA
/// `Array(body)` has, and what `IMassProperty::AddBodies(BodyListIn)` takes.
pub fn dispatch_array(items: &[IDispatch]) -> Var {
unsafe {
let psa = SafeArrayCreateVector(VT_VARIANT, 0, items.len() as u32);
if psa.is_null() {
return Var::empty();
}
for (i, d) in items.iter().enumerate() {
let one = Var::dispatch(d);
let idx = [i as i32];
// SafeArrayPutElement COPIES a VARIANT element, so `one` is
// released on drop without double-freeing the array's copy.
let _ = SafeArrayPutElement(psa, idx.as_ptr(), &one.0 as *const VARIANT as *const std::ffi::c_void);
}
build(VARENUM(VT_ARRAY.0 | VT_VARIANT.0), |v| {
(*v.Anonymous.Anonymous).Anonymous.parray = psa;
})
}
}
/// A TYPED object array: SAFEARRAY of VT_DISPATCH (VT_ARRAY|VT_DISPATCH),
/// the shape .NET's `object[] Objects` (IModelDocExtension::MultiSelect2)
/// marshals as. The VARIANT-wrapped form above was MEASURED on SOLIDWORKS
/// 2026 to select nothing (0.10.3).
pub fn dispatch_typed_array(items: &[IDispatch]) -> Var {
unsafe {
let psa = SafeArrayCreateVector(VT_DISPATCH, 0, items.len() as u32);
if psa.is_null() {
return Var::empty();
}
for (i, d) in items.iter().enumerate() {
let idx = [i as i32];
// For VT_DISPATCH the element IS the interface pointer (no extra
// indirection); SafeArrayPutElement AddRefs it.
let _ = SafeArrayPutElement(psa, idx.as_ptr(), d.as_raw() as *const std::ffi::c_void);
}
build(VARENUM(VT_ARRAY.0 | VT_DISPATCH.0), |v| {
(*v.Anonymous.Anonymous).Anonymous.parray = psa;
})
}
}
/// `[ref] i32` — the byref out-parameter shape SOLIDWORKS uses everywhere
/// (OpenDoc6 errors/warnings, Save3, ...). The i32 must outlive the call.
pub fn byref_i4(p: *mut i32) -> Var {
unsafe {
build(VARENUM(VT_I4.0 | VT_BYREF.0), |v| {
(*v.Anonymous.Anonymous).Anonymous.plVal = p
})
}
}
/// `[ref] BSTR` — CustomPropertyManager::Get4's two out-parameters.
pub fn byref_bstr(p: *mut BSTR) -> Var {
unsafe {
build(VARENUM(VT_BSTR.0 | VT_BYREF.0), |v| {
(*v.Anonymous.Anonymous).Anonymous.pbstrVal = p
})
}
}
/// `[ref] VARIANT` — Pack and Go's GetDocumentSaveToNames.
pub fn byref_variant(p: *mut VARIANT) -> Var {
unsafe {
build(VARENUM(VT_VARIANT.0 | VT_BYREF.0), |v| {
(*v.Anonymous.Anonymous).Anonymous.pvarVal = p
})
}
}
pub fn vt(&self) -> u16 {
unsafe { self.0.Anonymous.Anonymous.vt.0 }
}
pub fn is_null(&self) -> bool {
let vt = self.vt();
if vt == VT_EMPTY.0 || vt == VT_NULL.0 {
return true;
}
if vt == VT_DISPATCH.0 {
return self.as_dispatch().is_none();
}
false
}
pub fn as_i32(&self) -> Option<i32> {
unsafe { coerce(&self.0, VT_I4).map(|v| v.0.Anonymous.Anonymous.Anonymous.lVal) }
}
pub fn as_f64(&self) -> Option<f64> {
unsafe { coerce(&self.0, VT_R8).map(|v| v.0.Anonymous.Anonymous.Anonymous.dblVal) }
}
pub fn as_bool(&self) -> Option<bool> {
unsafe { coerce(&self.0, VT_BOOL).map(|v| v.0.Anonymous.Anonymous.Anonymous.boolVal.0 != 0) }
}
/// Anything printable, coerced to a string (None for EMPTY/NULL).
pub fn as_string(&self) -> Option<String> {
if self.is_null() {
return None;
}
unsafe {
coerce(&self.0, VT_BSTR)
.map(|v| v.0.Anonymous.Anonymous.Anonymous.bstrVal.to_string())
}
}
pub fn as_dispatch(&self) -> Option<IDispatch> {
unsafe {
let inner = &self.0.Anonymous.Anonymous;
if inner.vt.0 == VT_DISPATCH.0 {
(*inner.Anonymous.pdispVal).clone()
} else if inner.vt.0 == (VT_DISPATCH.0 | VT_BYREF.0) {
let p = inner.Anonymous.ppdispVal;
if p.is_null() {
None
} else {
(*p).clone()
}
} else {
None
}
}
}
/// An owned deep copy (VariantCopy → new refs / new BSTR), so the result and
/// `self` can both drop without a double-free. Used to lift a scalar into a
/// one-element array.
pub fn copy(&self) -> Var {
unsafe {
let mut dest = VARIANT::default();
if VariantCopy(&mut dest, &self.0).is_err() {
return Var::empty();
}
Var(dest)
}
}
/// The elements of a SAFEARRAY-carrying VARIANT, each boxed as a `Var`.
/// Handles the element types SOLIDWORKS returns: VARIANT (the usual),
/// DISPATCH/UNKNOWN (component lists), R8 (center of mass, boxes), BSTR, I4.
///
/// A NON-array scalar becomes a one-element vec — matching the PowerShell
/// `@(...)` coercion the sidecar relied on, so a single open document, a
/// single interference, or a lone configuration name is reported as a
/// one-item list, never as `[]`.
pub fn as_array(&self) -> Vec<Var> {
unsafe {
let inner = &self.0.Anonymous.Anonymous;
if inner.vt.0 & VT_ARRAY.0 == 0 {
return if self.is_null() { Vec::new() } else { vec![self.copy()] };
}
let psa: *mut SAFEARRAY = if inner.vt.0 & VT_BYREF.0 != 0 {
let pp = inner.Anonymous.pparray;
if pp.is_null() {
return Vec::new();
}
*pp
} else {
inner.Anonymous.parray
};
if psa.is_null() {
return Vec::new();
}
let (lb, ub) = match (SafeArrayGetLBound(psa, 1), SafeArrayGetUBound(psa, 1)) {
(Ok(l), Ok(u)) => (l, u),
_ => return Vec::new(),
};
let elem_vt = SafeArrayGetVartype(psa).map(|v| v.0).unwrap_or(VT_VARIANT.0);
let mut out = Vec::new();
for i in lb..=ub {
let idx = [i];
if elem_vt == VT_VARIANT.0 {
let mut v = VARIANT::default();
if SafeArrayGetElement(psa, idx.as_ptr(), &mut v as *mut _ as *mut c_void)
.is_ok()
{
out.push(Var(v));
}
} else if elem_vt == VT_DISPATCH.0 || elem_vt == VT_UNKNOWN.0 {
let mut p: *mut c_void = std::ptr::null_mut();
if SafeArrayGetElement(psa, idx.as_ptr(), &mut p as *mut _ as *mut c_void)
.is_ok()
&& !p.is_null()
{
// SafeArrayGetElement AddRef'd for us. Query for
// IDispatch (SOLIDWORKS objects are all dispatch); `unk`
// takes the ref SafeArrayGetElement added and releases it,
// Var::dispatch adds its own.
let unk = IUnknown::from_raw(p);
if let Ok(d) = unk.cast::<IDispatch>() {
out.push(Var::dispatch(&d));
}
}
} else if elem_vt == VT_R8.0 {
let mut d = 0f64;
if SafeArrayGetElement(psa, idx.as_ptr(), &mut d as *mut _ as *mut c_void)
.is_ok()
{
out.push(Var::r8(d));
}
} else if elem_vt == VT_BSTR.0 {
let mut b = BSTR::new();
if SafeArrayGetElement(psa, idx.as_ptr(), &mut b as *mut _ as *mut c_void)
.is_ok()
{
out.push(Var::bstr(&b.to_string()));
}
} else if elem_vt == VT_UI1.0 {
// A byte array: IModelDocExtension::GetPersistReference3's
// persistent id (0.10.2), the de-duplication key.
let mut b = 0u8;
if SafeArrayGetElement(psa, idx.as_ptr(), &mut b as *mut _ as *mut c_void)
.is_ok()
{
out.push(Var::i4(b as i32));
}
} else if elem_vt == VT_I4.0 {
let mut n = 0i32;
if SafeArrayGetElement(psa, idx.as_ptr(), &mut n as *mut _ as *mut c_void)
.is_ok()
{
out.push(Var::i4(n));
}
}
}
out
}
}
/// Best-effort JSON for a value we do not know the shape of (the escape
/// hatch, and anything SOLIDWORKS hands back loosely typed).
pub fn to_json(&self) -> serde_json::Value {
use serde_json::Value;
let vt = self.vt();
if self.is_null() {
return Value::Null;
}
if vt & VT_ARRAY.0 != 0 {
return Value::Array(self.as_array().iter().map(|v| v.to_json()).collect());
}
let base = vt & !VT_BYREF.0;
if base == VT_BOOL.0 {
return self.as_bool().map(Value::from).unwrap_or(Value::Null);
}
if base == VT_R8.0 {
return self.as_f64().map(Value::from).unwrap_or(Value::Null);
}
if base == VT_I4.0 {
return self.as_i32().map(Value::from).unwrap_or(Value::Null);
}
if base == VT_DISPATCH.0 {
return Value::from("<COM object>");
}
self.as_string().map(Value::from).unwrap_or(Value::Null)
}
}
/// A copy of `src` as type `to`, owned (and freed) by the returned Var.
/// VariantChangeType handles the same-type case as a plain copy, so there is no
/// special case to get wrong.
unsafe fn coerce(src: &VARIANT, to: VARENUM) -> Option<Var> {
let mut dest = VARIANT::default();
VariantChangeType(&mut dest, src, Default::default(), to).ok().map(|_| Var(dest))
}
/// A live IDispatch we can call by name.
#[derive(Clone)]
pub struct Disp(pub IDispatch);
impl Disp {
/// DISPID for a member name. A missing name is the single most useful COM
/// error to report well: it usually means the SOLIDWORKS version does not
/// have that API, not that the bridge is broken.
pub fn dispid(&self, name: &str) -> Result<i32, HostError> {
let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
let names = [PCWSTR(wide.as_ptr())];
let mut ids = [0i32];
unsafe {
self.0
.GetIDsOfNames(&GUID::zeroed(), names.as_ptr(), 1, 0, ids.as_mut_ptr())
.map_err(|e| {
HostError::detailed(
format!("SOLIDWORKS has no member named '{name}'"),
format!("{e}"),
)
})?;
}
Ok(ids[0])
}
/// QueryInterface for another DUAL interface of the same object (e.g. an
/// IFace2's IEntity), so late binding resolves THAT interface's names.
/// The caller must know `iid` is dual — a vtable-only interface called
/// through IDispatch slots would be a wild call.
pub fn query_dual(&self, iid: &GUID) -> Option<Disp> {
let mut p: *mut c_void = std::ptr::null_mut();
unsafe {
if self.0.query(iid, &mut p).is_err() || p.is_null() {
return None;
}
Some(Disp(IDispatch::from_raw(p)))
}
}
/// SOLIDWORKS is a single-threaded-apartment server with no message filter
/// of its own: while it is mid-rebuild or pumping its UI it REJECTS incoming
/// calls rather than queueing them. Retrying is the documented answer.
pub const RPC_E_CALL_REJECTED: i32 = 0x8001_0001u32 as i32;
pub const RPC_E_SERVERCALL_RETRYLATER: i32 = 0x8001_010Au32 as i32;
/// `invoke`, with a backoff for a BUSY SOLIDWORKS.
///
/// Without this a rejected call surfaced as a plain failure and, worse, as a
/// WRONG ANSWER: `GetDocuments` handed back an empty/null VARIANT (not an
/// error), `as_array()` turned that into an empty Vec, and `swx_docs`
/// answered `success:true` with zero documents while 51 were open. Every
/// title resolution and ownership check built on that read then concluded
/// "no open document is titled 'Assem24'" or "this bridge did not create it"
/// for a document made seconds earlier.
pub fn invoke(&self, name: &str, flags: DISPATCH_FLAGS, args: &[&Var]) -> Result<Var, HostError> {
let mut delay_ms = 100u64;
let mut total = 0u64;
loop {
match self.invoke_once(name, flags, args) {
Err(e) if e.code.as_deref() == Some("com_call_rejected") && total < 5_000 => {
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
total += delay_ms;
delay_ms = (delay_ms * 2).min(1_600);
}
other => return other,
}
}
}
/// Invoke `name` with `args` in source order (DISPPARAMS wants them
/// reversed; that is this function's job, not the caller's).
pub fn invoke_once(&self, name: &str, flags: DISPATCH_FLAGS, args: &[&Var]) -> Result<Var, HostError> {
let id = self.dispid(name)?;
// OWNERSHIP: DISPPARAMS wants an array of [in] VARIANTs, and Invoke does
// NOT take ownership of them. VARIANT has real drop glue in this crate
// (it clears via VariantClear), so the array MUST NOT be a second owner
// of each argument's contents — otherwise the array's drop and the
// caller's `Var` drop both VariantClear the same BSTR / IDispatch, which
// is a heap double-free and a premature Release of a live SOLIDWORKS
// interface. So the array holds ManuallyDrop bitwise copies: the `Var`
// arguments (borrowed here, still alive) remain the sole owners and are
// the only things that clear.
let mut raw: Vec<std::mem::ManuallyDrop<VARIANT>> = args
.iter()
.rev()
.map(|v| std::mem::ManuallyDrop::new(unsafe { std::ptr::read(&v.0) }))
.collect();
let mut put_id = DISPID_PROPERTYPUT;
let params = DISPPARAMS {
rgvarg: if raw.is_empty() {
std::ptr::null_mut()
} else {
raw.as_mut_ptr() as *mut VARIANT
},
rgdispidNamedArgs: if flags == DISPATCH_PROPERTYPUT {
&mut put_id
} else {
std::ptr::null_mut()
},
cArgs: raw.len() as u32,
cNamedArgs: u32::from(flags == DISPATCH_PROPERTYPUT),
};
let mut result = VARIANT::default();
let mut excep = EXCEPINFO::default();
let mut arg_err = 0u32;
let outcome = unsafe {
self.0.Invoke(
id,
&GUID::zeroed(),
0,
flags,
¶ms,
Some(&mut result),
Some(&mut excep),
Some(&mut arg_err),
)
};
// EXCEPINFO's BSTR fields are ManuallyDrop<BSTR> with no Drop of their
// own; on a failed Invoke they may hold allocated strings, so read and
// then free them (a null BSTR frees to a no-op, so this is safe always).
let (source, description) = (excep.bstrSource.to_string(), excep.bstrDescription.to_string());
unsafe {
std::mem::ManuallyDrop::drop(&mut excep.bstrSource);
std::mem::ManuallyDrop::drop(&mut excep.bstrDescription);
std::mem::ManuallyDrop::drop(&mut excep.bstrHelpFile);
}
if let Err(ref e) = outcome {
let code = e.code().0;
if code == Self::RPC_E_CALL_REJECTED || code == Self::RPC_E_SERVERCALL_RETRYLATER {
return Err(HostError {
message: format!("SOLIDWORKS was busy and rejected '{name}'"),
detail: Some(format!("{e}")),
code: Some("com_call_rejected".to_string()),
// A REJECTION, not a timeout: SOLIDWORKS answered promptly
// that it was busy. Reporting it as a timeout would supply
// half of a `host_app_hung` verdict for a message loop that
// is running perfectly.
timed_out: false,
// A rejected call did nothing at all, so it made nothing.
created: None,
data: Some(serde_json::json!({
"hresult": format!("0x{:08X}", code as u32),
"call": name,
})),
});
}
}
outcome.map_err(|e| {
let mut detail = format!("{e}");
if !source.is_empty() {
detail.push_str(&format!(" [{source}]"));
}
if !description.is_empty() {
detail.push_str(&format!(": {description}"));
}
if arg_err != 0 || !args.is_empty() {
detail.push_str(&format!(" (argument index {arg_err})"));
}
// AND THE SAME FACTS, STRUCTURED. The prose is for a person; the
// HRESULT is what a person diagnoses a translator from, and it was
// being flattened into a sentence that callers then truncated.
HostError::detailed(format!("SOLIDWORKS rejected '{name}'"), detail).with_data(
serde_json::json!({
"call": name,
"hresult": format!("0x{:08X}", e.code().0 as u32),
"source": if source.is_empty() { serde_json::Value::Null } else { serde_json::json!(source) },
"description": if description.is_empty() { serde_json::Value::Null } else { serde_json::json!(description) },
"argumentIndex": arg_err,
}),
)
})?;
Ok(Var(result))
}
/// `obj.Name(...)` — method or property get.
pub fn call(&self, name: &str, args: &[&Var]) -> Result<Var, HostError> {
self.invoke(name, CALL, args)
}
/// `obj.Name` where the result must be another object.
pub fn obj(&self, name: &str, args: &[&Var]) -> Result<Option<Disp>, HostError> {
Ok(self.call(name, args)?.as_dispatch().map(Disp))
}
/// `obj.Name = value`.
pub fn put(&self, name: &str, value: &Var) -> Result<(), HostError> {
self.invoke(name, DISPATCH_PROPERTYPUT, &[value])?;
Ok(())
}
/// A property/method whose failure does not matter (best-effort settings).
pub fn try_put(&self, name: &str, value: &Var) {
let _ = self.put(name, value);
}
pub fn string(&self, name: &str, args: &[&Var]) -> Option<String> {
self.call(name, args).ok().and_then(|v| v.as_string())
}
pub fn int(&self, name: &str, args: &[&Var]) -> Option<i32> {
self.call(name, args).ok().and_then(|v| v.as_i32())
}
pub fn float(&self, name: &str, args: &[&Var]) -> Option<f64> {
self.call(name, args).ok().and_then(|v| v.as_f64())
}
pub fn flag(&self, name: &str, args: &[&Var]) -> Option<bool> {
self.call(name, args).ok().and_then(|v| v.as_bool())
}
}
#[cfg(test)]
mod tests {
use super::*;
/// "NOTHING" IS NOT "OMITTED", and the difference is what broke every import.
///
/// This runs only where `variant.rs` compiles — a Windows build — which is
/// also the only place the encoding means anything. It is the one test that
/// inspects the bytes: the simulator has no VARIANTs to look at, so it cannot
/// tell these two apart no matter how it is written.
#[test]
fn nothing_is_a_null_dispatch_and_not_an_empty_variant() {
let nothing = Var::null_dispatch();
let omitted = Var::empty();
unsafe {
let n = &*nothing.0.Anonymous.Anonymous;
let o = &*omitted.0.Anonymous.Anonymous;
assert_eq!(
n.vt, VT_DISPATCH,
"an [in] object parameter set to Nothing must be VT_DISPATCH; SOLIDWORKS answered \
Type mismatch (0x80020005) to VT_EMPTY on LoadFile4's importData, live 2026-09-17"
);
assert!(
n.Anonymous.pdispVal.is_none(),
"Nothing means a NULL pdispVal, not a live interface pointer"
);
assert_eq!(o.vt, VT_EMPTY, "Var::empty must stay VT_EMPTY — it is a different thing");
}
}
}