denise-activex 0.2.0

COM/ActiveX shim for Denise, so legacy Windows hosts can embed the control.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
//! The type library: a `.tlb` built at registration from the dispatch table.
//!
//! # Why this exists
//!
//! Answering `GetTypeInfoCount` with zero is honest and costs nothing for the
//! hosts that bind names late — VBScript, JScript, VB6 through an `Object`
//! variable, MFC's `COleDispatchDriver`, every OLE container. It costs PowerShell
//! entirely: PowerShell builds its member table from type information and will not
//! ask for a name it has not been told about, so a control with none is adapted as
//! a bare `System.__ComObject` and `$panel.Caption` fails before a single COM call
//! is made.
//!
//! `CreateDispTypeInfo` was tried first, as a way to answer without a library.
//! It cannot: the description it builds is vtable-shaped — `TKIND_INTERFACE`, not
//! `TKIND_DISPATCH` — and PowerShell looks for a dispinterface, does not find one,
//! and produces an object with no members and no complaint. Nothing in the method
//! table changes the kind. So this is the real thing.
//!
//! # Built rather than shipped
//!
//! There is no `.idl` and no MIDL step. `DllRegisterServer` calls
//! [`build`] to write `denise_activex.tlb` beside the DLL, from
//! [`crate::dispatch::entries`] — the same table `Invoke` reads. A library
//! compiled from separate source could disagree with the implementation; one
//! generated from the implementation's own table cannot.
//!
//! The cost is a second file to deploy. `RegisterTypeLib` records its full path,
//! so it has to stay next to the DLL.

use windows::Win32::Foundation::E_FAIL;
use windows::Win32::System::Com::{
    CC_STDCALL, ELEMDESC, ELEMDESC_0, FUNC_DISPATCH, FUNCDESC, FUNCFLAGS, IDLDESC,
    IMPLTYPEFLAG_FDEFAULT, IMPLTYPEFLAG_FSOURCE, INVOKE_FUNC, INVOKE_PROPERTYGET,
    INVOKE_PROPERTYPUT, ITypeInfo, ITypeLib, SYSKIND, TKIND_COCLASS, TKIND_DISPATCH, TYPEDESC,
    TYPEDESC_0,
};
use windows::Win32::System::Ole::{
    CreateTypeLib2, ICreateTypeInfo, LoadRegTypeLib, LoadTypeLibEx, PARAMDESC, REGKIND_NONE,
    REGKIND_REGISTER, TYPEFLAG_FCANCREATE, TYPEFLAG_FDISPATCHABLE, UnRegisterTypeLib,
};
use windows::Win32::System::Variant::VARENUM;
use windows_core::{BSTR, GUID, Interface, PCWSTR};

use crate::dispatch::{self, PUT};
use crate::server::CLSID_DENISE_PANEL;

/// The type library's own id.
///
/// Generated once and never changed: it is written into the registry, and a
/// compiled early-bound host stores it.
pub const LIBID_DENISE: GUID = GUID::from_u128(0x5CA2_EE57_C922_483E_8FDA_B0A8_B3D3_B195);

/// The incoming dispinterface — what a script reaches when it names a property.
pub const DIID_DENISE_PANEL: GUID = GUID::from_u128(0x4C51_48FF_09F3_4C34_9B77_00C8_50E1_F940);

/// The library's version. Bumping this makes a new registry key, so it is part of
/// the contract rather than a build number.
pub const VERSION: (u16, u16) = (1, 0);

/// Which registry key `RegisterTypeLib` writes the path under.
///
/// A decision made by the compiler, not by the machine: a 32-bit build registers
/// under `win32` and a 64-bit one under `win64`, and Windows on ARM64 is 64-bit.
/// A host loads the one matching its own word size, so a mismatch here is a
/// library that registers and never loads.
const fn syskind() -> SYSKIND {
    #[cfg(target_pointer_width = "64")]
    {
        windows::Win32::System::Com::SYS_WIN64
    }
    #[cfg(not(target_pointer_width = "64"))]
    {
        windows::Win32::System::Com::SYS_WIN32
    }
}

/// Labels a failure with the step that produced it.
///
/// Every call in here can answer `TYPE_E_ELEMENTNOTFOUND`, and on its own that
/// says "a type is missing" about a type you can see in the source. Naming the
/// step turns one CI round trip into an answer instead of a guess — which is the
/// difference this file was written to stop paying for.
fn step<T>(what: &str, result: windows_core::Result<T>) -> windows_core::Result<T> {
    result.map_err(|e| windows_core::Error::new(e.code(), format!("{what}: {}", e.message())))
}

/// `IDispatch`'s own description, from the standard OLE library.
///
/// A dispinterface *inherits* `IDispatch` — that is what makes it dispatchable —
/// and `LayOut` will not resolve one that does not say so. The description has to
/// come from stdole2, which is registered on every Windows machine, because a
/// type library can only refer to types it can name.
fn idispatch_type_info() -> windows_core::Result<ITypeInfo> {
    /// stdole2's library id, and version 2.0.
    const LIBID_STDOLE: GUID = GUID::from_u128(0x0002_0430_0000_0000_C000_0000_0000_0046);
    // SAFETY: constants; the out-parameter is the binding's.
    let stdole = step("LoadRegTypeLib(stdole2)", unsafe {
        LoadRegTypeLib(&LIBID_STDOLE, 2, 0, 0)
    })?;
    // SAFETY: `stdole` is live and the IID outlives the call.
    step("stdole2::IDispatch", unsafe {
        stdole.GetTypeInfoOfGuid(&windows::Win32::System::Com::IDispatch::IID)
    })
}

/// Makes `info` inherit `IDispatch`, which is what a dispinterface is.
fn inherit_idispatch(info: &ICreateTypeInfo) -> windows_core::Result<()> {
    let idispatch = idispatch_type_info()?;
    let mut href = 0u32;
    // SAFETY: `idispatch` is live and `href` receives the reference. The binding
    // declares the out-parameter `*const`, which is a quirk of the generated
    // signature rather than of the API.
    unsafe {
        step("AddRefTypeInfo(IDispatch)", {
            info.AddRefTypeInfo(&idispatch, &mut href as *mut u32 as *const u32)
        })?;
        step("AddImplType(IDispatch)", info.AddImplType(0, href))?;
    }
    Ok(())
}

/// The library file that belongs beside `dll`.
///
/// `…\denise_activex.dll` becomes `…\denise_activex.tlb`.
pub fn path_beside(dll: &str) -> String {
    match dll.rfind('.') {
        Some(dot) => format!("{}.tlb", &dll[..dot]),
        None => format!("{dll}.tlb"),
    }
}

/// Writes the library to `path`.
pub fn build(path: &str) -> windows_core::Result<()> {
    let wide = wide(path);
    // SAFETY: `wide` is NUL-terminated and live for the call.
    let library = step("CreateTypeLib2", unsafe {
        CreateTypeLib2(syskind(), PCWSTR(wide.as_ptr()))
    })?;

    // SAFETY: every one of these takes values live for its own call.
    unsafe {
        step("library.SetGuid", library.SetGuid(&LIBID_DENISE))?;
        step("library.SetName", library.SetName(&BSTR::from("Denise")))?;
        step(
            "library.SetVersion",
            library.SetVersion(VERSION.0, VERSION.1),
        )?;
        // Locale-neutral. The names are ASCII and there is nothing to localise;
        // claiming a locale would make a host in another one look elsewhere.
        step("library.SetLcid", library.SetLcid(0))?;
    }

    // SAFETY: `library` is live; each call builds one type into it.
    let panel = step("CreateTypeInfo(DDenisePanel)", unsafe {
        library.CreateTypeInfo(&BSTR::from("DDenisePanel"), TKIND_DISPATCH)
    })?;
    describe_panel(&panel)?;

    // SAFETY: as above.
    let events = step("CreateTypeInfo(DDenisePanelEvents)", unsafe {
        library.CreateTypeInfo(&BSTR::from("DDenisePanelEvents"), TKIND_DISPATCH)
    })?;
    describe_events(&events)?;

    // Laid out before anything refers to them. A type under construction has no
    // resolved layout, and `AddRefTypeInfo` on one answers
    // `TYPE_E_ELEMENTNOTFOUND` — which reads like a missing type rather than an
    // unfinished one, and is how this failed the first time it ran.
    // SAFETY: both are live and fully described by the calls above.
    unsafe {
        step("panel.LayOut", panel.LayOut())?;
        step("events.LayOut", events.LayOut())?;
    }

    // SAFETY: as above.
    let coclass = step("CreateTypeInfo(Panel)", unsafe {
        library.CreateTypeInfo(&BSTR::from("Panel"), TKIND_COCLASS)
    })?;
    describe_coclass(&coclass, &panel, &events)?;
    // SAFETY: the class is described; laying it out resolves its two references.
    step("coclass.LayOut", unsafe { coclass.LayOut() })?;

    // SAFETY: writes the file. Everything above is held until this returns.
    step("SaveAllChanges", unsafe { library.SaveAllChanges() })
}

/// The members a script can reach.
fn describe_panel(info: &ICreateTypeInfo) -> windows_core::Result<()> {
    // SAFETY: a live builder and a GUID that outlives the call.
    unsafe {
        info.SetGuid(&DIID_DENISE_PANEL)?;
        info.SetVersion(VERSION.0, VERSION.1)?;
        // `FDISPATCHABLE` is what makes this reachable through `IDispatch` rather
        // than through a vtable a dispinterface does not have.
        step(
            "panel.SetTypeFlags",
            info.SetTypeFlags(TYPEFLAG_FDISPATCHABLE.0 as u32),
        )?;
    }
    inherit_idispatch(info)?;

    for (index, entry) in dispatch::entries().iter().enumerate() {
        // A put takes the value being assigned; a get and a method take nothing.
        // Held in a local that outlives `AddFuncDesc`, which copies what it reads.
        let mut argument = [ELEMDESC {
            tdesc: TYPEDESC {
                Anonymous: TYPEDESC_0 {
                    lptdesc: core::ptr::null_mut(),
                },
                vt: VARENUM(entry.vt),
            },
            Anonymous: ELEMDESC_0 {
                paramdesc: PARAMDESC::default(),
            },
        }];

        // A put returns nothing; everything else returns what it was declared to.
        let returns = if entry.flags == PUT {
            dispatch::VOID
        } else {
            entry.vt
        };

        let description = FUNCDESC {
            memid: entry.dispid,
            lprgscode: core::ptr::null_mut(),
            lprgelemdescParam: if entry.arguments == 0 {
                core::ptr::null_mut()
            } else {
                argument.as_mut_ptr()
            },
            // `FUNC_DISPATCH`, because these are reached by dispid rather than by
            // slot: a dispinterface has no vtable to be at an offset into.
            funckind: FUNC_DISPATCH,
            invkind: match entry.flags {
                dispatch::GET => INVOKE_PROPERTYGET,
                dispatch::PUT => INVOKE_PROPERTYPUT,
                _ => INVOKE_FUNC,
            },
            callconv: CC_STDCALL,
            cParams: entry.arguments as i16,
            cParamsOpt: 0,
            oVft: 0,
            cScodes: 0,
            elemdescFunc: ELEMDESC {
                tdesc: TYPEDESC {
                    Anonymous: TYPEDESC_0 {
                        lptdesc: core::ptr::null_mut(),
                    },
                    vt: VARENUM(returns),
                },
                Anonymous: ELEMDESC_0 {
                    idldesc: IDLDESC::default(),
                },
            },
            wFuncFlags: FUNCFLAGS(0),
        };

        // SAFETY: `description` and `argument` are live locals, and `AddFuncDesc`
        // copies what it is given rather than retaining the pointers — which is
        // the promise that a previous attempt at this got wrong, freeing the
        // buffers a description still pointed at and crashing the machine reading
        // it back.
        // SAFETY: as described above.
        let added = unsafe { info.AddFuncDesc(index as u32, &description) };
        step(&format!("panel.AddFuncDesc[{index}] {}", entry.name), added)?;

        // The member's name, and only that.
        //
        // A property put has one parameter — the value being assigned — and it is
        // *not* named here: the property's own name covers it, and passing a
        // second name is `TYPE_E_ELEMENTNOTFOUND`, which reads like a missing type
        // rather than a name too many. That is what this cost to find out: the get
        // at index 0 took one name and the put at index 1 refused two.
        let name = wide(entry.name);
        let names = [PCWSTR(name.as_ptr())];
        // SAFETY: both buffers outlive the call, and `names` describes them.
        let named = unsafe { info.SetFuncAndParamNames(index as u32, &names) };
        step(
            &format!(
                "panel.SetFuncAndParamNames[{index}] {} with {} name(s), flags {}",
                entry.name,
                names.len(),
                entry.flags
            ),
            named,
        )?;
    }
    Ok(())
}

/// The events the control raises.
fn describe_events(info: &ICreateTypeInfo) -> windows_core::Result<()> {
    // SAFETY: a live builder and a GUID that outlives the call.
    unsafe {
        info.SetGuid(&crate::automation::DIID_DENISE_PANEL_EVENTS)?;
        info.SetVersion(VERSION.0, VERSION.1)?;
        step(
            "events.SetTypeFlags",
            info.SetTypeFlags(TYPEFLAG_FDISPATCHABLE.0 as u32),
        )?;
    }
    inherit_idispatch(info)?;

    for (index, event) in dispatch::EVENTS.iter().enumerate() {
        let description = FUNCDESC {
            memid: event.dispid,
            lprgscode: core::ptr::null_mut(),
            lprgelemdescParam: core::ptr::null_mut(),
            funckind: FUNC_DISPATCH,
            invkind: INVOKE_FUNC,
            callconv: CC_STDCALL,
            cParams: 0,
            cParamsOpt: 0,
            oVft: 0,
            cScodes: 0,
            elemdescFunc: ELEMDESC {
                tdesc: TYPEDESC {
                    Anonymous: TYPEDESC_0 {
                        lptdesc: core::ptr::null_mut(),
                    },
                    vt: VARENUM(dispatch::VOID),
                },
                Anonymous: ELEMDESC_0 {
                    idldesc: IDLDESC::default(),
                },
            },
            wFuncFlags: FUNCFLAGS(0),
        };
        // SAFETY: `description` is a live local and is copied by the call.
        step("events.AddFuncDesc", unsafe {
            info.AddFuncDesc(index as u32, &description)
        })?;

        let name = wide(event.name);
        // SAFETY: `name` outlives the call.
        step("events.SetFuncAndParamNames", unsafe {
            info.SetFuncAndParamNames(index as u32, &[PCWSTR(name.as_ptr())])
        })?;
    }
    Ok(())
}

/// The class, and which of the two interfaces is which.
///
/// This is the part a host reads to answer "what happens when I create a
/// `Denise.Panel`": the default interface is what it binds to, and the default
/// *source* is what `WithEvents` in VB6 or `Register-ObjectEvent` in PowerShell
/// hooks up to.
fn describe_coclass(
    info: &ICreateTypeInfo,
    panel: &ICreateTypeInfo,
    events: &ICreateTypeInfo,
) -> windows_core::Result<()> {
    // SAFETY: a live builder; `CLSID_DENISE_PANEL` outlives the call.
    unsafe {
        info.SetGuid(&CLSID_DENISE_PANEL)?;
        info.SetVersion(VERSION.0, VERSION.1)?;
        info.SetTypeFlags(TYPEFLAG_FCANCREATE.0 as u32)?;
    }

    for (index, (part, flags)) in [
        (panel, IMPLTYPEFLAG_FDEFAULT),
        (events, IMPLTYPEFLAG_FDEFAULT | IMPLTYPEFLAG_FSOURCE),
    ]
    .into_iter()
    .enumerate()
    {
        let part: ITypeInfo = part.cast()?;
        let mut href = 0u32;
        // SAFETY: `part` is live, and `href` receives the reference. The binding
        // declares the out-parameter `*const`, which is a quirk of the generated
        // signature rather than of the API — `AddRefTypeInfo` writes through it.
        unsafe {
            step("coclass.AddRefTypeInfo", {
                info.AddRefTypeInfo(&part, &mut href as *mut u32 as *const u32)
            })?;
            step("coclass.AddImplType", info.AddImplType(index as u32, href))?;
            step(
                "coclass.SetImplTypeFlags",
                info.SetImplTypeFlags(index as u32, flags),
            )?;
        }
    }
    Ok(())
}

/// Registers the library at `path` with the system.
pub fn register(path: &str) -> windows_core::Result<()> {
    let wide = wide(path);
    // SAFETY: `wide` is NUL-terminated and live. `REGKIND_REGISTER` both loads it
    // and writes the `TypeLib` keys, including the platform subkey that says which
    // word size this file is for.
    unsafe { LoadTypeLibEx(PCWSTR(wide.as_ptr()), REGKIND_REGISTER) }?;
    Ok(())
}

/// Removes it again.
///
/// The file itself is left where it is. It was written next to the DLL, it is
/// rewritten by the next registration, and deleting files on the way out is a
/// bigger promise than a server should make.
pub fn unregister() -> windows_core::Result<()> {
    // SAFETY: the library id and version are constants; the locale matches what
    // `build` set.
    unsafe { UnRegisterTypeLib(&LIBID_DENISE, VERSION.0, VERSION.1, 0, syskind()) }
}

/// The dispinterface a host should be handed, from the registered library.
///
/// Falls back to the file beside the DLL, so a control that was created without
/// `regsvr32` — from a test, or by a host that registered it per-user — still
/// describes itself.
pub fn panel_type_info(fallback: Option<&str>) -> windows_core::Result<ITypeInfo> {
    // SAFETY: constants, and an out-parameter the binding owns.
    if let Ok(library) = unsafe { LoadRegTypeLib(&LIBID_DENISE, VERSION.0, VERSION.1, 0) } {
        // SAFETY: `library` is live and the GUID outlives the call.
        if let Ok(info) = unsafe { library.GetTypeInfoOfGuid(&DIID_DENISE_PANEL) } {
            return Ok(info);
        }
    }

    let path = fallback.ok_or(E_FAIL)?;
    let wide = wide(path);
    // SAFETY: `wide` is live. `REGKIND_NONE` loads without touching the registry,
    // which is what makes this usable from a test that must not need privileges.
    let library: ITypeLib = unsafe { LoadTypeLibEx(PCWSTR(wide.as_ptr()), REGKIND_NONE) }?;
    // SAFETY: `library` is live and the GUID outlives the call.
    unsafe { library.GetTypeInfoOfGuid(&DIID_DENISE_PANEL) }
}

/// A NUL-terminated UTF-16 buffer, which is what every `W` entry point wants.
fn wide(text: &str) -> Vec<u16> {
    text.encode_utf16().chain(core::iter::once(0)).collect()
}