Skip to main content

Crate denise_activex

Crate denise_activex 

Source
Expand description

The COM/ActiveX shim, so legacy Windows hosts can embed the Denise control.

VB6, MFC, Delphi and WinForms all reach a control the same way: a class id in the registry, a DLL that answers DllGetClassObject, and an object implementing the OLE control interfaces. denise_win32 already provides the window such an object would host; this crate is the wrapper around it.

§Status

The server is here: the four Dll* exports, a class factory, and a control implementing IOleObject, IOleInPlaceObject, IOleWindow, IOleControl, IPersistStreamInit, IDispatch, IViewObject2 and the connection point that carries its events. A container can instantiate it, site it, activate it in place — at which point it creates a real denise_win32 child window — script it by name, sink its events, and tear it down again.

It can also be asked to draw without any of that, which is what a form editor does: IViewObject2::Draw renders the tree from the current property values into whatever device context the container passes, with no site and no window. Without it a control dropped on a form is a blank rectangle until the form runs. The geometry that decides where the picture goes is in view, outside cfg(windows) with the rest of the arithmetic.

registry, himetric and dispatch are the halves that can be tested without Windows, and they are also the halves that most often go wrong: a control fails to appear in a host’s toolbox for one of about four reasons, all of them a missing or wrong registry value, and none of them producing an error anywhere. So those lists are data, and the tests check them as data.

§Scripting it

$panel = New-Object -ComObject Denise.Panel
$panel.Caption = "Hei"
$panel.Caption

That works because there is a type library now. Hosts that bind names late — VBScript, JScript, VB6 through an Object variable, MFC’s COleDispatchDriver, every OLE container — never needed one and are unchanged. PowerShell did: it builds its member table from type information and will not ask for a name it has not been told about. See the typelib module for what that took and what was tried first.

The surface is still short. A type library makes each member discoverable, which is a reason to have fewer good ones rather than a licence to add more.

MemberDispid
Text1property, read/write — the field’s contents
Caption2property, read/write — the heading
Enabled3property, read/write — whether the field and button take input
Refresh4method — repaint everything
Change1event — somebody typed in the field
Click-600event — the button was pressed (DISPID_CLICK)
$panel = New-Object -ComObject Denise.Panel
$panel.Caption = "Hei"
$panel.Caption

Events arrive through a connection point. The library names DDenisePanelEvents as the class’s default source, so a host that reads it can wire them up by itself — WithEvents in VB6, Register-ObjectEvent in PowerShell. By hand it is DIID_DENISE_PANEL_EVENTS passed to IConnectionPointContainer::FindConnectionPoint, then advise an object implementing IDispatch: there is no vtable to match and nothing to compile against, only Invoke with one of the two dispids above.

§Safe for scripting

The control claims it, through IObjectSafety and through the two component categories in the registry, because hosts are split on which one they ask.

The claim is worth stating precisely, since claiming it carelessly is how ActiveX earned its reputation. The scriptable surface is the six rows above: two strings, a boolean and a repaint. Nothing in it opens a file, spawns a process, reads the registry, resolves a host name, or takes a pointer or a window handle from the caller — and Load reads nothing, so untrusted data has nothing to be untrusted with. A script that drives this control as far as it goes has changed some text on a panel.

That is a claim about the surface as it stands. Add a member that reaches outside the control and it has to be argued again rather than inherited; the safety module is where the argument lives, next to the code that makes it.

§What is not here yet

A form editor that has actually hosted it. The two pieces one needs are both here — a type library to read and a design-time view to draw — and both are checked on every push. Neither has been in front of VB6 or an MFC dialog editor.

§What has been verified

On Windows 11 ARM64: registered with regsvr32, instantiated through CoCreateInstance, sited, activated in place, and rendering — with text typed into it, including AltGr and dead keys. Then scripted: Caption, Text and Enabled set and read by name through GetIDsOfNames and Invoke, a sink advised on the connection point, and Change and Click arriving at it.

The design-time view too, out of the registered server and before the control was sited: a card inset from the frame with the heading inside it, printed as text by examples/host.rs because that path never reaches a screen. What that adds to the unit tests is the registry — they construct the control directly, so only this says the installed DLL exposes IViewObject2 at all.

And from PowerShell, which is the host that needed the type library:

TypeName: System.__ComObject#{4c5148ff-09f3-4c34-9b77-00c850e1f940}

Name    MemberType Definition
----    ---------- ----------
Refresh Method     void Refresh ()
Caption Property   string Caption () {get} {set}
Enabled Property   bool Enabled () {get} {set}
Text    Property   string Text () {get} {set}

Worth reading closely, because it confirms four separate things: the class points at the dispinterface, Refresh returns VT_VOID rather than the VT_EMPTY that once made a host unwrap a null, each property was reassembled from the two entries a library stores it as, and the types survived.

The part worth naming is the re-entrancy. The click handler assigns to Caption and the change handler reads Text, both from inside the event the control raised — a few hundred round trips over eighteen clicks and every keystroke of a sentence, with the control’s RefCell borrowed around all of them.

examples/host.rs is the container that did it, and it goes through the registry rather than around it, so what it proves is the registered server.

§Registering it

regsvr32 denise_activex.dll
regsvr32 /u denise_activex.dll

Registration also writes denise_activex.tlb beside the DLL and registers it. The registry records that file’s full path, so the two have to stay together — moving the DLL and re-registering is fine; moving it and not re-registering leaves a library nobody can load.

The class id, the library id and the two interface ids are generated once and never change: a host stores them in a form file or a compiled binary, so a new one on every build would break every project that ever embedded the control.

Re-exports§

pub use registry::CLSID_TEXT;
pub use registry::FRIENDLY_NAME;
pub use registry::MISC_STATUS;
pub use registry::PROG_ID;
pub use registry::VERSION;
pub use registry::VERSION_INDEPENDENT_PROG_ID;

Modules§

connections
Cookies, and the table they index.
dispatch
The automation surface, as data: what a script can name and what it means.
himetric
OLE’s unit of length, and back.
registry
What DllRegisterServer writes, as data rather than as code.
safety
The scripting safety claim, and what it actually asserts.
typelib
The type library: a .tlb built at registration from the dispatch table.
view
Where a design-time picture goes, worked out before any of it reaches GDI.

Structs§

DenisePanel
The panel a container embeds.

Constants§

CLSID_DENISE_PANEL
The control’s class id, as a GUID.
DIID_DENISE_PANEL_EVENTS
The outgoing dispinterface a container sinks events on.

Functions§

DllCanUnloadNow
Whether COM may unload this DLL.
DllGetClassObject⚠
Hands a class object to COM.
DllRegisterServer
Writes every registry value the control needs to be findable.
DllUnregisterServer
Removes them again.