denise_activex/lib.rs
1//! The COM/ActiveX shim, so legacy Windows hosts can embed the Denise control.
2//!
3//! VB6, MFC, Delphi and WinForms all reach a control the same way: a class id in
4//! the registry, a DLL that answers `DllGetClassObject`, and an object
5//! implementing the OLE control interfaces. [`denise_win32`] already provides the
6//! window such an object would host; this crate is the wrapper around it.
7//!
8//! # Status
9//!
10//! The server is here: the four `Dll*` exports, a class factory, and a control
11//! implementing `IOleObject`, `IOleInPlaceObject`, `IOleWindow`, `IOleControl`,
12//! `IPersistStreamInit`, `IDispatch`, `IViewObject2` and the connection point
13//! that carries its events. A container can instantiate it, site it, activate it
14//! in place — at which point it creates a real [`denise_win32`] child window —
15//! script it by name, sink its events, and tear it down again.
16//!
17//! It can also be asked to draw without any of that, which is what a form editor
18//! does: `IViewObject2::Draw` renders the tree from the current property values
19//! into whatever device context the container passes, with no site and no
20//! window. Without it a control dropped on a form is a blank rectangle until the
21//! form runs. The geometry that decides where the picture goes is in
22//! [`view`], outside `cfg(windows)` with the rest of the arithmetic.
23//!
24//! `registry`, `himetric` and `dispatch` are the halves that can be tested
25//! without Windows, and they are also the halves that most often go wrong: a
26//! control fails to appear in a host's toolbox for one of about four reasons, all
27//! of them a missing or wrong registry value, and none of them producing an error
28//! anywhere. So those lists are data, and the tests check them as data.
29//!
30//! # Scripting it
31//!
32//! ```text
33//! $panel = New-Object -ComObject Denise.Panel
34//! $panel.Caption = "Hei"
35//! $panel.Caption
36//! ```
37//!
38//! That works because there is a type library now. Hosts that bind names late —
39//! VBScript, JScript, VB6 through an `Object` variable, MFC's
40//! `COleDispatchDriver`, every OLE container — never needed one and are unchanged.
41//! PowerShell did: it builds its member table from type information and will not
42//! ask for a name it has not been told about. See the `typelib` module for what that took
43//! and what was tried first.
44//!
45//! The surface is still short. A type library makes each member *discoverable*,
46//! which is a reason to have fewer good ones rather than a licence to add more.
47//!
48//! | Member | Dispid | |
49//! |---|---|---|
50//! | `Text` | 1 | property, read/write — the field's contents |
51//! | `Caption` | 2 | property, read/write — the heading |
52//! | `Enabled` | 3 | property, read/write — whether the field and button take input |
53//! | `Refresh` | 4 | method — repaint everything |
54//! | `Change` | 1 | event — somebody typed in the field |
55//! | `Click` | -600 | event — the button was pressed (`DISPID_CLICK`) |
56//!
57//! ```text
58//! $panel = New-Object -ComObject Denise.Panel
59//! $panel.Caption = "Hei"
60//! $panel.Caption
61//! ```
62//!
63//! Events arrive through a connection point. The library names
64//! `DDenisePanelEvents` as the class's default source, so a host that reads it can
65//! wire them up by itself — `WithEvents` in VB6, `Register-ObjectEvent` in
66//! PowerShell. By hand it is [`DIID_DENISE_PANEL_EVENTS`] passed to
67//! `IConnectionPointContainer::FindConnectionPoint`, then advise an object
68//! implementing `IDispatch`: there is no vtable to match and nothing to compile
69//! against, only `Invoke` with one of the two dispids above.
70//!
71//! # Safe for scripting
72//!
73//! The control claims it, through `IObjectSafety` and through the two component
74//! categories in the registry, because hosts are split on which one they ask.
75//!
76//! The claim is worth stating precisely, since claiming it carelessly is how
77//! ActiveX earned its reputation. The scriptable surface is the six rows above:
78//! two strings, a boolean and a repaint. Nothing in it opens a file, spawns a
79//! process, reads the registry, resolves a host name, or takes a pointer or a
80//! window handle from the caller — and `Load` reads nothing, so untrusted *data*
81//! has nothing to be untrusted with. A script that drives this control as far as
82//! it goes has changed some text on a panel.
83//!
84//! That is a claim about the surface as it stands. Add a member that reaches
85//! outside the control and it has to be argued again rather than inherited; the
86//! [`safety`] module is where the argument lives, next to the code that makes it.
87//!
88//! # What is not here yet
89//!
90//! **A form editor that has actually hosted it.** The two pieces one needs are
91//! both here — a type library to read and a design-time view to draw — and both
92//! are checked on every push. Neither has been in front of VB6 or an MFC dialog
93//! editor.
94//!
95//! # What has been verified
96//!
97//! On Windows 11 ARM64: registered with `regsvr32`, instantiated through
98//! `CoCreateInstance`, sited, activated in place, and rendering — with text typed
99//! into it, including AltGr and dead keys. Then scripted: `Caption`, `Text` and
100//! `Enabled` set and read **by name** through `GetIDsOfNames` and `Invoke`, a sink
101//! advised on the connection point, and `Change` and `Click` arriving at it.
102//!
103//! The design-time view too, out of the registered server and before the control
104//! was sited: a card inset from the frame with the heading inside it, printed as
105//! text by `examples/host.rs` because that path never reaches a screen. What that
106//! adds to the unit tests is the registry — they construct the control directly,
107//! so only this says the *installed* DLL exposes `IViewObject2` at all.
108//!
109//! And from PowerShell, which is the host that needed the type library:
110//!
111//! ```text
112//! TypeName: System.__ComObject#{4c5148ff-09f3-4c34-9b77-00c850e1f940}
113//!
114//! Name MemberType Definition
115//! ---- ---------- ----------
116//! Refresh Method void Refresh ()
117//! Caption Property string Caption () {get} {set}
118//! Enabled Property bool Enabled () {get} {set}
119//! Text Property string Text () {get} {set}
120//! ```
121//!
122//! Worth reading closely, because it confirms four separate things: the class
123//! points at the dispinterface, `Refresh` returns `VT_VOID` rather than the
124//! `VT_EMPTY` that once made a host unwrap a null, each property was reassembled
125//! from the two entries a library stores it as, and the types survived.
126//!
127//! The part worth naming is the re-entrancy. The click handler assigns to
128//! `Caption` and the change handler reads `Text`, both from inside the event the
129//! control raised — a few hundred round trips over eighteen clicks and every
130//! keystroke of a sentence, with the control's `RefCell` borrowed around all of
131//! them.
132//!
133//! `examples/host.rs` is the container that did it, and it goes through the
134//! registry rather than around it, so what it proves is the *registered* server.
135//!
136//! # Registering it
137//!
138//! ```text
139//! regsvr32 denise_activex.dll
140//! regsvr32 /u denise_activex.dll
141//! ```
142//!
143//! Registration also writes `denise_activex.tlb` beside the DLL and registers it.
144//! The registry records that file's full path, so **the two have to stay
145//! together** — moving the DLL and re-registering is fine; moving it and not
146//! re-registering leaves a library nobody can load.
147//!
148//! The class id, the library id and the two interface ids are generated once and
149//! never change: a host stores them in a form file or a compiled binary, so a new
150//! one on every build would break every project that ever embedded the control.
151
152pub mod connections;
153pub mod dispatch;
154pub mod himetric;
155pub mod registry;
156pub mod safety;
157pub mod view;
158
159#[cfg(windows)]
160mod automation;
161#[cfg(windows)]
162mod control;
163#[cfg(windows)]
164mod factory;
165#[cfg(windows)]
166mod model;
167#[cfg(windows)]
168mod server;
169#[cfg(windows)]
170pub mod typelib;
171#[cfg(windows)]
172mod variant;
173
174#[cfg(windows)]
175pub use automation::DIID_DENISE_PANEL_EVENTS;
176#[cfg(windows)]
177pub use control::DenisePanel;
178#[cfg(windows)]
179pub use server::{
180 CLSID_DENISE_PANEL, DllCanUnloadNow, DllGetClassObject, DllRegisterServer, DllUnregisterServer,
181};
182
183pub use registry::{
184 CLSID_TEXT, FRIENDLY_NAME, MISC_STATUS, PROG_ID, VERSION, VERSION_INDEPENDENT_PROG_ID,
185};
186
187/// Compiles the examples in this crate's README, so they cannot drift from the API
188/// they claim to demonstrate. Never built except under `cargo test --doc`.
189#[cfg(doctest)]
190#[doc = include_str!("../README.md")]
191struct Readme;