Registration and Bitness Pitfalls in COM/OCX/ActiveX Development
· Updated: · Go Komura · COM, ActiveX, OCX, Visual Studio, Windows Development, 32bit, 64bit, Interop
Revision history (2 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.22170286)
- Added an explanation of DllSurrogate for reaching a 32-bit in-proc COM server from a 64-bit process: why a successful regsvr32 still leaves a 64-bit process unable to load a 32-bit InprocServer32, how adding an AppID to the CLSID and an empty DllSurrogate under that AppID hosts the DLL in dllhost.exe without writing a new EXE, why the dllhost.exe that starts follows the DLL's bitness rather than the client's, why a registered LocalServer32 wins over the surrogate, and how to verify the result. It also states plainly that a surrogate does not erase the bitness difference: only the same-process constraint goes away, and calls become out-of-process with marshaling and IPC cost. Read the version before this update (DOI: 10.5281/zenodo.21614619)
- First published
Cite this article(DOI: 10.5281/zenodo.21614618)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Registration and Bitness Pitfalls in COM/OCX/ActiveX Development. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614618 https://comcomponent.com/en/blog/2026/04/15/001-com-ocx-activex-pitfalls-visual-studio-bitness-admin-rights/
- DOI (latest version)
- 10.5281/zenodo.21614618
- DOI (this version)
- 10.5281/zenodo.22220450
In COM component and OCX / ActiveX projects, the things that trip you up tend to live at the boundaries of runtime environment, registration, host, and privileges - not in the code itself.
The typical symptoms look like this.
- The build succeeds, but at startup you get
0x80040154. - It works on your own development machine but not on other PCs.
- It runs fine at runtime, but only the Visual Studio Designer dies.
- It works when launched as administrator but breaks under normal privileges.
- You keep running
regsvr32, yet somehow nothing gets fixed.
These are less individual bugs and more a state where some assumption of COM is misaligned somewhere.
If you first want to sort out the terminology itself - COM / ActiveX / OCX - start with What Are COM / ActiveX / OCX? - The Differences and Relationships Explained to get the big picture. This article covers the next stage: where you actually get stuck in practice, including Visual Studio bitness and administrator-rights issues.
1. The Conclusion First
Put in terms that matter in real work, it comes down to this.
- COM / OCX / ActiveX trouble is more often caused by mismatches in bitness (32bit / 64bit), registration location, host, and privileges than by code logic.
- Visual Studio 2022 is a 64bit process, so design-time integration that assumed 32bit and used to just work will break as is.12
regsvr32is not a magic command that registers everything. It is for native in-proc COM servers (DLL / OCX). To expose .NET Framework assemblies to COM you useRegasm.exe, and for .NET 5+ / .NET 6+ / .NET 8+ you register the generated.comhost.dll.3456- “It works as administrator, so it’s fine” is dangerous. It is not unusual that the component is only visible by accident through a per-user registration, or that registration that should be done by the installer was done by hand only on the development machine.378
So when you deal with COM / OCX / ActiveX, the safe approach is to look at these four axes first.
- Which process hosts the component
- Whether that process is 32bit or 64bit
- Where it is registered (HKCU / HKLM, 32bit view / 64bit view)
- Whether the operation or execution requires administrator rights
Knowledge map for this article
Trouble with COM, OCX, and ActiveX tends to come from a mismatch in bitness, registration tool, registration scope, or privileges rather than from a defect in the code. In Visual Studio 2022 devenv.exe is a 64-bit process, so it cannot load an x86-only ActiveX control directly at design time; regsvr32 targets native in-proc COM servers, while registering a .NET Framework assembly requires Regasm.exe instead. HKEY_CLASSES_ROOT is a merged view of HKLM and HKCU, and the CLSID subkey is separate for 32-bit and 64-bit through Wow6432Node, so a control that is registered only in the other view or only per-user is by itself enough to produce Class not registered. In an STA, the message loop and CoInitializeEx are the very mechanism that delivers calls, and stopping it deadlocks.
flowchart LR
accTitle: COM and ActiveX registration pitfalls
accDescr: Diagram showing how registration trouble with COM, OCX, and ActiveX relates to the move of Visual Studio 2022 to 64-bit, the WOW64 registry redirector, choosing between registration tools such as regsvr32 and Regasm, the HKCU and HKLM registration scopes, and the STA message loop
com_registration_pitfalls["COM/OCX Registration Pitfalls"]
visual_studio_2022["Visual Studio 2022"]
wow64_registry_redirector["WOW64 Registry Redirector"]
activex["ActiveX"]
ocx["OCX"]
bitness_match_requirement["Bitness Match Requirement"]
com["COM (Component Object Model)"]
hkcu_classes["HKEY_CURRENT_USER\Software\Classes"]
clsid["CLSID (Class ID)"]
wow6432node["WOW6432Node"]
regsvr32["regsvr32"]
in_proc_com["In-Proc COM (DLL Server)"]
regasm["Regasm.exe"]
dotnet_framework[".NET Framework"]
dotnet_comhost[".NET 5+ COM Hosting (.comhost.dll)"]
dotnet[".NET (Core and Later)"]
file_system_redirector["WOW64 File System Redirector"]
admin_rights["Administrator Privileges"]
class_not_registered_error["0x80040154 (Class Not Registered)"]
per_user_registration["Per-User COM Registration"]
hkcr["HKEY_CLASSES_ROOT (HKCR)"]
hklm_classes["HKEY_LOCAL_MACHINE\Software\Classes"]
per_machine_registration["Per-Machine COM Registration"]
activex_license["ActiveX Design-Time and Run-Time Licenses"]
aximp["AxImp (ActiveX Control Importer)"]
type_library["Type Library (TLB)"]
axhost["AxHost"]
com_apartment_model["COM apartment model (STA/MTA)"]
message_loop["Message Loop"]
coinitializeex["CoInitializeEx"]
sta_blocking_deadlock["STA Thread Blocking Deadlock"]
com_marshaling["Marshaling"]
reg_free_com["Registration-Free COM"]
assembly_manifest["Assembly Manifest (Reg-Free COM)"]
com_localserver["COM LocalServer (out-of-process server)"]
progid["ProgID (Programmatic Identifier)"]
com_registration_pitfalls -->|"uses"| wow64_registry_redirector
com_registration_pitfalls -->|"uses"| visual_studio_2022
visual_studio_2022 -.->|"incompatible with"| activex
ocx -->|"requires"| bitness_match_requirement
ocx -->|"implements"| activex
com -->|"uses"| hkcu_classes
clsid -.->|"stored in"| wow6432node
regsvr32 -->|"uses"| in_proc_com
regasm -->|"recommended for"| dotnet_framework
regsvr32 -->|"not recommended for"| dotnet_framework
dotnet_comhost -->|"recommended for"| dotnet
regsvr32 -->|"uses"| file_system_redirector
regsvr32 -.->|"requires"| admin_rights
wow64_registry_redirector -.->|"may cause"| class_not_registered_error
per_user_registration -.->|"may cause"| class_not_registered_error
hkcr -->|"inherits the contents of"| hklm_classes
hkcr -->|"inherits the contents of"| hkcu_classes
per_machine_registration -->|"requires"| admin_rights
activex -.->|"requires"| activex_license
aximp -->|"requires"| type_library
aximp -->|"uses"| axhost
axhost -->|"implements"| activex
com_apartment_model -.->|"requires"| message_loop
com_apartment_model -->|"requires"| coinitializeex
message_loop -->|"prevents"| sta_blocking_deadlock
com_apartment_model -->|"requires"| com_marshaling
reg_free_com -->|"uses"| assembly_manifest
reg_free_com -->|"mitigates"| com_registration_pitfalls
com_localserver -->|"requires"| clsid
progid -->|"requires"| clsid
hklm_classes -->|"uses"| wow64_registry_redirector
clsid -->|"stored in"| hkcr
activex -.->|"requires"| com_apartment_model
regsvr32 -->|"uses"| dotnet_comhost
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (34 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. Working Backwards from Symptoms
| Symptom | First suspect | Common real cause |
|---|---|---|
0x80040154 Class not registered |
Not registered | Often it is actually “registered only on the other bitness side” or “registered only for that one user” |
DllRegisterServer failed: 0x80070005 |
Insufficient privileges | Trying to register as a standard user, or registering in a post-build step without elevation |
| Only the Designer breaks in VS2022 | Designer constraints | The 64bit Visual Studio cannot directly load a 32bit COM / ActiveX component |
| Works only when launched as administrator | Privilege issue | The real problem is usually less about privileges and more about misaligned registration scope or installation design |
| A 64bit app cannot call a 32bit OCX | A constraint of how COM works | An in-proc server can only be loaded into a host of the same bitness |
| Works on the UI thread but hangs on a background thread | Threading model | Violated assumptions about STA / MTA, CoInitializeEx, and the message loop |
Officially, 0x80040154 is REGDB_E_CLASSNOTREG - literally Class not registered.9
And 0x80070005 from regsvr32 is officially documented as the case where you lack administrator rights and cannot write to the registry or System32.10
The important thing here is not to take the error name at face value.
For example, Class not registered does not necessarily mean “not registered at all” - it also occurs when the component is registered in a different registry view or registered somewhere visible only to that particular user.117
3. Getting Stuck on Visual Studio Bitness
3.1. Visual Studio 2022 Became 64bit
This is the single most common stumbling point in COM / ActiveX development today.
In Visual Studio 2022, devenv.exe is 64bit only.1
As a result, in the WinForms design-time experience, Visual Studio itself cannot directly load 32bit components. Microsoft explicitly states that because Visual Studio 2022 is a 64bit process, it cannot load 32bit .NET / COM / ActiveX components.2
In other words, what used to hold together loosely under these assumptions —
- the project is x86
- the referenced ActiveX is also x86
- Visual Studio itself is 32bit
— twists in VS2022 into:
- the app can still run as x86 at runtime
- but the Designer runs inside the 64bit Visual Studio
The result is the state where the app is alive at runtime, but only the Designer crashes, and the cause is hard to see. The runtime host is the application’s process while the design-time host is Visual Studio’s process, and because the hosts are different things, their bitness is different too.2
3.2. Switching to AnyCPU Does Not Necessarily Fix It
This is another common misconception.
AnyCPU is not magic that makes your dependencies neutral too.
As Microsoft’s own explanation notes, even a component that looks like AnyCPU will still be a problem at Visual Studio 2022 design time if, further down the chain, it references a COM / ActiveX component pinned to 32bit.2
So when switching to AnyCPU does not make the error go away, it is faster to suspect:
- whether there is a 32bit native dependency further down from that assembly
- whether the ActiveX / OCX is pinned to x86
- whether there is code that is only loaded at design time
3.3. The System32 and SysWOW64 Trap
On Windows x64, the names and the reality do not line up, which causes endless confusion.
Microsoft Learn explains that on x64 Windows, %windir%\System32 is for 64bit applications. The 32bit side is redirected elsewhere by the WOW64 file system redirector.12
The registry behaves the same way: the WOW64 registry redirector presents separate logical views to 32bit and 64bit code.11
For that reason, when you troubleshoot locally, it is safer to be explicit about which bitness of regsvr32 you are using.
# To register a 64bit DLL / OCX
C:\Windows\System32\regsvr32.exe vendor.ocx
# To register a 32bit DLL / OCX on x64 Windows
C:\Windows\SysWOW64\regsvr32.exe vendor.ocx
The nasty case is registering with the regsvr32 from the wrong side. The registration itself succeeds, but the target process cannot see the component.
What you end up with is:
regsvr32succeeded- but the app still reports
0x80040154 - the registry looks like the entry is there
- but you are looking at a different view
That is the state you land in.1113
3.4. regsvr32 Cannot Register Everything
This is another area full of misconceptions.
Native in-proc COM servers normally support self-registration by exporting DllRegisterServer / DllUnregisterServer.3
regsvr32 is the tool for exactly those DLLs / OCXs.4
On the other hand, if you want to use a .NET Framework assembly from COM, the standard tool is Regasm.exe. Microsoft Learn likewise states that Regasm.exe is used to register assemblies for use with COM.514
Furthermore, COM exposure in .NET 5+ / .NET 6+ / .NET 8+ works a little differently: when you build with <EnableComHosting>true</EnableComHosting>, a *.comhost.dll is generated, and that is what you register with regsvr32.6
Roughly categorized, it looks like this.
| What you want to expose | Typical registration mechanism |
|---|---|
| Native C++ DLL / OCX | regsvr32 |
| .NET Framework assembly exposed to COM | Regasm.exe |
| .NET 5+ / 6+ / 8+ COM exposure | Register the generated .comhost.dll with regsvr32 |
You run Regasm.exe from a Developer Command Prompt or Developer PowerShell. These three forms cover most of what you need.14
:: 1) Register the public classes inside the assembly
regasm myTest.dll
:: 2) Also generate and register a type library (for clients that need a TLB, such as VBA / VB6)
regasm myTest.dll /tlb:myTest.tlb
:: 3) Write the entries out to a .reg file for inspection instead of changing the registry directly
regasm myTest.dll /regfile:myTest.reg
To undo a registration, use /unregister. A type library registered with /tlb is unregistered by specifying /tlb and /unregister together.
regasm myTest.dll /tlb:myTest.tlb /unregister
There are two constraints here that are easy to walk into.14
- An assembly that does not go into the GAC needs
/codebase. Because/codebaserecords the file path at registration time in the registry, moving the assembly afterwards makes activation fail. Microsoft also strongly recommends that any assembly you register with/codebasebe strong-named. /regfilecannot be combined with/unregisteror/tlb. What/regfilewrites out is only the entries for the managed classes; theTypeLibIDandInterfaceIDare not emitted. If you assume “shipping the.regis all the registration you need”, it will fall short.
Mixing these up produces a common pattern:
- run
regsvr32against a managed DLL - naturally,
DllRegisterServeris not found - conclude - wrongly - that “the DLL is corrupted”
In the world that needs type libraries (VBA / VB6 / some early-binding clients), generating and registering the TLB is yet another separate problem. With .NET Framework you can generate and register a type library via Regasm.exe /tlb, and Microsoft explains that “registering types” and “registering the type library” are separate activities.15
3.5. When You Want a 32bit In-Proc Server Out of a 64bit Process — DllSurrogate
Sections 3.3 and 3.4 were about whether the location and the means of registration line up. Here I want to add one more way out, for when the registration is correct but the bitness does not match.
The conclusion first.
- Even when
regsvr32succeeded, a 64bit process cannot load a 32bitInprocServer32. That is not a defect in the registration; it is the very premise of in-proc. - If you want to use that DLL from the 64bit side without writing a new EXE, the minimal remedy is to attach an
AppIDto the CLSID and write an empty-stringDllSurrogateinto thatAppIDkey.16 - What starts then is a
dllhost.exeof the bitness of the DLL thatInprocServer32points to, not of the client side.
Why It Cannot Work While It Stays In-Proc
“A 64bit app cannot call a 32bit OCX”, listed in the symptom table in chapter 2, wears the same face as 0x80040154 but is something different underneath.
InprocServer32 literally designates “a server that runs inside the calling process”. A 64bit process loading a 32bit DLL written there will not happen no matter how you fix the registry. The fact that the registry views are split, as in 3.3, ultimately traces back to this same constraint.
So from here on, the story is not “fix the registration” but “move it out into a separate process”.
What a Surrogate Does
DllSurrogate is the designation for putting an in-proc DLL server into a surrogate process and exposing it as a Local Server.17
If you make the value an empty string, the default surrogate that ships with Windows (dllhost.exe) is used.18
For a 32bit DLL, a 32bit surrogate starts, and the DLL is loaded in-proc, exactly as before, inside it. The 64bit client touches the object from outside that process, through a proxy.
This is easy to misunderstand, so let me put it plainly.
A surrogate does not erase the bitness difference. What goes away is only the constraint that “it has to sit in the same process”. The calls become out-of-proc, and the cost of marshaling and inter-process communication is added on top. As a precondition, the types you exchange must be marshalable as well (IDispatch, a registered proxy / stub, or a type the standard marshaler can handle).
One more thing. What a surrogate handles well is an automation object that is complete with method calls and events. A visual OCX that you paste onto a form and have it draw assumes that its window lives inside the host’s process, so this remedy alone does not settle it.
flowchart TB
accTitle: Whether activation lands on in-proc or on the surrogate
accDescr: A diagram showing that when a request is made with a CLSCTX that includes in-proc, the object is first loaded in-proc if the CLSID in the same view has InprocServer32, that when it does not and an EXE server registration exists the EXE starts before the surrogate, that when there is no EXE registration and an empty DllSurrogate exists a dllhost of the DLL side bitness starts, and that when none of them exist activation is not possible.
req["A client requests activation"] --> ctx["Requested with a CLSCTX that includes in-proc"]
ctx --> q1{"Does the CLSID in the same view have InprocServer32"}
q1 -->|"Yes"| inproc["Loaded in-proc as before"]
q1 -->|"No"| q2{"Is there an EXE server registration"}
q2 -->|"Yes"| exe["The EXE starts before the surrogate"]
q2 -->|"No"| q3{"Is there an empty DllSurrogate"}
q3 -->|"Yes"| host["A dllhost of the DLL side bitness starts"]
q3 -->|"No"| err["Cannot activate, 0x80040154"]
Figure 1: Activation branches first on whether an InprocServer32 of the same bitness is visible; only when it is not does it fall to an EXE server and then to an empty DllSurrogate.
The Minimum Set of Registrations
Microsoft Learn lists the conditions for a DLL server to sit in a surrogate as follows.16
- The CLSID key has an
AppIDvalue, and the correspondingAppIDkey exists CLSCTX_LOCAL_SERVERis set in the activation call, and the CLSID key does not haveLocalServer32/LocalServer/LocalService- The CLSID key has
InprocServer32 - The DLL that
InprocServer32points to actually exists - There is a
DllSurrogatevalue under theAppIDkey
If you want to run that DLL on its own in a single surrogate, making the AppID the same GUID as the CLSID is the way Microsoft recommends writing it.16
Three lines are enough for the registration. Below is the case of using a 32bit COM DLL from a 64bit host, and the GUIDs are placeholders. It assumes that InprocServer32 has already been put in by the SysWOW64 side regsvr32 (see 3.3).
:: Run this in an elevated command prompt
set CLSID={11111111-2222-3333-4444-555555555555}
set APPID={11111111-2222-3333-4444-555555555555}
:: 1) Attach an AppID to the CLSID. Everything under CLSID is separate for 32/64, so state the view explicitly
:: For the reverse direction, using a 64bit COM DLL from a 32bit host, read this as /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /v AppID /t REG_SZ /d "%APPID%" /f /reg:32
:: 2) Create the corresponding AppID key. Classes\AppID is shared between 32/64, so no view needs to be stated
reg add "HKLM\SOFTWARE\Classes\AppID\%APPID%" /f
:: 3) Make DllSurrogate an empty REG_SZ. Omitting /d creates it as an empty string
reg add "HKLM\SOFTWARE\Classes\AppID\%APPID%" /v DllSurrogate /t REG_SZ /f
Why It Has to Be an Empty String
DllSurrogate is a REG_SZ, and the value itself is the path to the surrogate. Making it an empty string (or NULL) makes the system default surrogate be used, while writing a path makes it try to start the custom surrogate at that path.1718
In other words, writing in the path to dllhost.exe out of a sense of helpfulness is counterproductive: leaving it empty is itself the instruction to “use the default one”.
Note that HKLM\SOFTWARE\Classes\AppID is a key that is shared between 32bit and 64bit from Windows 7 / Windows Server 2008 R2 onwards, with no distinction of view.19
Everything under CLSID, on the other hand, is split by view, so for a 32bit server the AppID value has to be attached on the CLSID side of the 32bit view. When it does not take effect, always check these two places together.
This looks as if it contradicts “if it is registered only on the 32bit side, a 64bit process gets 0x80040154” from 3.3 and 6.3, but that is a story about in-proc. In out-of-proc activation, when neither the client nor the server states a bitness preference, COM looks for a server of the bitness that matches the client, and if there is none it starts the server of the other bitness.20
That is why this procedure holds with the CLSID side left in the 32bit view. There is no need to copy InprocServer32 into the 64bit view.
The Bitness of the dllhost.exe That Starts
This is the point that is misunderstood most often.
The bitness of the surrogate is decided not by the client side, but by the DLL side that InprocServer32 points to. Since the DLL is read in-proc inside the surrogate, that is only natural.
| COM DLL read inside it | dllhost.exe that starts |
|---|---|
| 32bit | %SystemRoot%\SysWOW64\dllhost.exe |
| 64bit | %SystemRoot%\System32\dllhost.exe |
If you look at the command line in Task Manager or Process Explorer, it stands there in the form dllhost.exe /Processid:{...}. The GUID there is the AppID, not the CLSID. Look for it expecting a CLSID and you will not find it, so be careful.
If LocalServer32 Is There, the EXE Wins
Microsoft Learn states explicitly that when LocalServer / LocalServer32 / LocalService is present, starting the EXE server or the service always takes precedence over loading the DLL into a surrogate.16
In other words, a surrogate is the way out when there is no EXE server. Adding DllSurrogate to a CLSID that already has an EXE server registered will not get that path used. And what takes precedence here does so strictly over the surrogate, not over in-proc.
It is reassuring to pin down the effect on existing callers too.
Even if you add DllSurrogate, a client that creates the object with a CLSCTX that includes in-proc stays in-proc exactly as before, as long as the bitness matches. That is because when you pass several CLSCTX values OR’d together they are tried in enumeration order (in-proc → local → remote), and at the in-proc stage, if the InprocServer32 key is there, that is what gets used.20
Calls that pass both in-proc and local, such as CLSCTX_ALL or CLSCTX_SERVER, fall into this. Conversely, the places that call with only CLSCTX_INPROC_SERVER will not be routed to the surrogate even after you add DllSurrogate.
And seen from a 64bit client, the InprocServer32 you put into the 32bit view does not exist in the 64bit view. The in-proc stage passes straight through as “no such key”, so it proceeds directly to activation on the local side (the surrogate). It is not that it is failing while trying to load a DLL of a different bitness.2019
How to Verify
Whether it went in is most reliably confirmed by reading it back with the view stated explicitly.
reg query "HKLM\SOFTWARE\Classes\CLSID\{11111111-2222-3333-4444-555555555555}\InprocServer32" /ve /reg:32
reg query "HKLM\SOFTWARE\Classes\CLSID\{11111111-2222-3333-4444-555555555555}" /v AppID /reg:32
reg query "HKLM\SOFTWARE\Classes\AppID\{11111111-2222-3333-4444-555555555555}" /v DllSurrogate
Do not skip the first line. reg add creates the specified key if it is not there, so if you mistype the GUID or register into a different view, you end up with an empty CLSID key carrying only the AppID value, and the second line still passes. Only once you have checked that InprocServer32 is in the same view too have you actually confirmed anything.
For DllSurrogate, it is correct if it comes out with the value still empty. After that, create the object from a 64bit client and watch whether the SysWOW64 side dllhost.exe starts.
- Still
0x80040154→ recheck whether theAppIDvalue is attached on the CLSID side of the 32bit view, and whether the DLL thatInprocServer32points to actually exists dllhost.exestarts but the call dies → check the marshaling preconditions (IDispatch/ proxy-stub / the standard marshaler)- A different EXE starts →
LocalServer32or something like it is still left on that CLSID
That Is as Far as “Registration” Goes
Whether a surrogate is enough, or whether you should write your own helper EXE on the 32bit side, is not a matter of registration but a matter of how you choose the architecture. That side is sorted out in 5.2, “You Want to Bring a 32-bit OCX to the 64-bit Side”, of How to Handle ActiveX / OCX Today - A Keep / Wrap / Replace Decision Table.
4. Getting Stuck on Administrator Rights
4.1. Registering in a Post-Build Step That Only Succeeds as Administrator
In Visual Studio C++ builds, calling regsvr32.exe from a build event or custom build step is perfectly possible. Microsoft Learn even shows registration with regsvr32.exe as an example post-build event.21
However, what is possible and what is safe are two different things.
Running regsvr32 as a standard user can fail with 0x80070005 because you cannot write to the registry or System32. Microsoft’s KB likewise attributes the cause to lacking administrator rights.10
What tends to go wrong here is:
- launch Visual Studio with normal privileges and the build passes
- but only the post-build registration fails
- the failure in the log goes unnoticed
- a previous, stale registration is still in place, so locally it happens to work
- on a clean environment, of course, it does not
The basic remedy for this kind of project is to separate build from registration.
- The build only produces binaries
- Registration is done by an explicit install step / script / installer
- In CI, put “steps that require registration” in a separate job from the build
That separation alone prevents a great deal of breakage.
4.2. Mixing Per-User and Per-Machine Registration Breaks Things
If your only mental model is “COM looks at HKCR”, this is where you get stuck.
In reality, as Microsoft Learn explains, COM looks at HKEY_CURRENT_USER\Software\Classes first, and then processes machine-wide information.3
Also, HKEY_CLASSES_ROOT is a merged view of HKLM\Software\Classes and HKCU\Software\Classes.722
Which means situations like this happen routinely:
- developer A registered the component manually under their own user
- it works under A’s account
- it does not work for developer B
- it does not work for the service account either
- running it as administrator changes the behavior
Furthermore, Microsoft states that applications that require administrator rights should register their dependent COM objects into the per-machine COM configuration store at install time.87
So in a development team, you should make this distinction explicit.
- Is this a development-only registration used by just your own user?
- Is this a production registration used by every user on the machine?
- Is this a registration that services or elevated applications will look up?
Leave this vague, and you end up with stories like “for some reason it only works as administrator” or “it works in the Explorer extension but not in the service”.
4.3. Always Running Visual Studio “as Administrator” Is Not a Solution
There are genuinely cases where it is necessary. But if you keep Visual Studio permanently elevated, you can end up hiding, behind IDE elevation, problems that should be solved by an installer or registration script.
On top of that, Visual Studio itself treats per-user extensions differently when elevated. Microsoft Learn describes a setting whereby running Visual Studio elevated disables per-user extensions.23
So as an operating practice:
- do day-to-day development with normal privileges
- run only the steps that need registration in an explicitly elevated Developer Command Prompt / PowerShell / installer
- if something “only reproduces as administrator”, treat that premise itself as something to specify and sort out
That will save you trouble later.
5. Pitfalls Specific to ActiveX / OCX
5.1. Design-Time and Run-Time Licenses Are Separate
A quietly troublesome aspect of OCX / ActiveX is licensing.
Especially with older ActiveX controls, the design-time license and run-time license can be separate. The MFC ActiveX documentation describes the mechanism of separating design-time and run-time via license files and license keys.2425
In this world, things like the following happen:
- it can be used at runtime
- but when you try to drop it on a form, you get “no license”
- it can be placed on development machine A
- but not on development machine B
Errors like “License information for this component not found” or “you don’t have an appropriate license” are not rare with old ActiveX.26
5.2. The Moment It Sits on a WinForms Form, a Wrapper Is Already Involved
When you use ActiveX from WinForms, Windows Forms is not hosting the ActiveX control directly.
As the Microsoft Learn documentation for Aximp.exe explains, the ActiveX Control Importer generates a WinForms wrapper from the COM type library and treats it as an AxHost-based control.2728
So the problem is not one layer but several:
- the original OCX / ActiveX itself
- the type library
- the generated interop / wrapper
- the WinForms Designer / runtime
That is why things like these happen:
- swapping the vendor OCX changed the event signatures
- re-adding the reference regenerated the wrapper and produced a huge diff
- the generated interop differs subtly from one development machine to another
Seeing the control appear in Choose Toolbox Items is no guarantee.
Whether the Designer can place it, whether events arrive at runtime, and whether the wrapper holds together on the deployment target are safer to verify separately.
5.3. Underestimate STA / MTA and the Message Loop, and Things Freeze
First, three terms worth defining.
| Term | Meaning |
|---|---|
| Apartment | A logical grouping of COM objects and threads under the same concurrency rules. An object can belong to only one apartment |
| STA (single-threaded apartment) | An apartment that exactly one thread belongs to. Calls into that object always execute on that one thread |
| Marshaling | The mechanism COM uses to bridge a call when an interface pointer is passed across apartments. It goes through a proxy and a stub |
COM must be initialized with CoInitializeEx on every thread that uses it. Microsoft Learn explicitly states that each thread that uses COM must call CoInitializeEx individually.29
Also, an STA (single-threaded apartment) requires a message loop.2930
Why a Missing Message Loop Makes Things Freeze
Memorizing only the conclusion here does not generalize, so it is worth opening up the mechanism one level.
For each STA, COM creates one hidden window of a window class called OleMainThreadWndClass. When another apartment calls that object, the call is not a direct function call: it arrives as a window message addressed to that hidden window. Once the STA thread pulls the message off and dispatches it, that window procedure invokes the matching method on the interface.30
In other words, the message loop is not “good practice to have” - it is the delivery mechanism for calls themselves.
sequenceDiagram
participant Caller as Caller on another thread
participant Queue as Message queue of the hidden window
participant STA as STA thread
participant Obj as OCX / ActiveX
Caller->>Queue: The method call is posted as a message
STA->>Queue: The message loop pulls it off
Queue->>Obj: The window procedure invokes the matching method
Obj-->>Caller: The return value comes back
Note over STA,Obj: If the message loop is stopped<br/>this pull never happens and the caller keeps waiting
Figure 2: A call from another thread arrives as a message to the hidden window, and only once the message loop pulls it off does it reach the object.
That is why blocking an STA thread makes things freeze. Writing Task.Wait() or Task.Result on the UI thread, or waiting on WaitOne, stops messages from being pulled off, so COM callbacks and cross-apartment calls are never delivered, and you get a deadlock.30
For the same reason, you must not simply copy the interface pointer of an STA object to another thread. Copying it means that a call that should have been delivered to the STA thread executes directly on a different thread, producing concurrency the object never expected. When crossing apartments, marshal with CoMarshalInterThreadInterfaceInStream and CoGetInterfaceAndReleaseStream.30
UI-oriented OCX / ActiveX components in particular usually assume STA, so you get:
- it works on the UI thread
- it freezes when offloaded to
Task.Runor the ThreadPool - events never come back
- it reproduces only occasionally
— a thoroughly unpleasant class of bug.
On top of that, in STA there is the rule that you must not simply copy interface pointers to another thread - they must be marshaled if needed.2930
These bugs are far less friendly than 0x80040154: all you see is hangs, no response, occasional crashes, so they eat just as much time as the registry-related trouble.
6. A Field-Proven Order of Elimination
In practice, rather than diving deep right away, slicing the problem in this order is faster.
6.1. First Pin Down “Which Process, and What Bitness”
This is the first thing to look at.
- What is the host (Visual Studio Designer / your own app / Office / Access / Explorer / a browser-compatibility environment)
- Is that host 32bit or 64bit
- Is the target DLL / OCX 32bit or 64bit
- Is it in-proc or out-of-proc
If you start looking at the registry before these four are settled, there is nothing to tell you which view to look at, and the investigation spins its wheels. Pin this down first.
6.2. Next, Confirm “the Kind of Registration”
The next thing to look at is what is supposed to be registered with what.
- Native DLL / OCX →
regsvr32 - .NET Framework COM exposure →
Regasm.exe - .NET 5+ / 6+ / 8+ COM exposure →
.comhost.dll - A DLL that simply has no self-registration → not a job for
regsvr32
This classification alone stops a great deal of friendly fire.
6.3. Only Then Look at “Where It Got Registered”
Looking at plain HKCR is not enough.
HKCU\Software\ClassesHKLM\Software\Classes- the 32bit / 64bit registry views, if relevant
- the target ProgID / CLSID / TypeLib
InprocServer32/LocalServer32- ThreadingModel
- the actual path of the referenced DLL
“It exists in HKCR” is insufficient - it only means something once you have also lined up who is looking, at what bitness, and through which view.711
The Key Paths Worth Looking At
Open HKEY_CLASSES_ROOT\CLSID\{...} and you see only what is visible to the bitness and the user of the tool you happen to be running. For triage, look at the four pre-merge locations separately. Everything under CLSID is subject to WOW64 redirection, and the physical location of the 32bit side is Wow6432Node under Classes.19
| What you want to see | Registry path |
|---|---|
| Machine-wide / 64bit view | HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{CLSID} |
| Machine-wide / 32bit view | HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Wow6432Node\CLSID\{CLSID} |
| That user only / 64bit view | HKEY_CURRENT_USER\SOFTWARE\Classes\CLSID\{CLSID} |
| That user only / 32bit view | HKEY_CURRENT_USER\SOFTWARE\Classes\Wow6432Node\CLSID\{CLSID} |
| Resolving a CLSID from a ProgID | The default value of HKEY_CLASSES_ROOT\{ProgID}\CLSID |
regedit.exe on Windows 10 / 11 is a 64bit process, so you can open both views directly by typing the paths above as they are. Paste a path into the address bar and it jumps to that location.
Note that a path containing Wow6432Node is the physical location, and Microsoft advises against touching it directly from application code. Reading it during a manual investigation is fine, but from scripts and applications it is safer to use the view specification described below.11
Checking from the Command Line
With reg.exe you can state the view explicitly using /reg:32 and /reg:64.
reg query "HKLM\SOFTWARE\Classes\CLSID\{00000000-0000-0000-0000-000000000000}\InprocServer32" /reg:32
reg query "HKLM\SOFTWARE\Classes\CLSID\{00000000-0000-0000-0000-000000000000}\InprocServer32" /reg:64
To see all four locations at once from PowerShell, pass the view to RegistryKey.OpenBaseKey. The result is not dragged around by PowerShell’s own bitness, which makes this the reliable choice for triage.
# Specify the ProgID you want to investigate in one place
$progId = 'Vendor.Control.1'
# 1) Resolve the CLSID from the ProgID (HKCR is the merged view of HKLM and HKCU)
$clsid = (Get-ItemProperty -Path "Registry::HKEY_CLASSES_ROOT\$progId\CLSID" -Name '(default)' -ErrorAction Stop).'(default)'
"ProgID $progId -> CLSID $clsid"
# 2) Check all four locations: 2 hives x 2 views
foreach ($hive in [Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryHive]::CurrentUser) {
foreach ($view in [Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32) {
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive, $view)
$key = $base.OpenSubKey("SOFTWARE\Classes\CLSID\$clsid\InprocServer32")
if ($null -eq $key) {
'{0,-12} {1,-11} : not registered' -f $hive, $view
}
else {
'{0,-12} {1,-11} : {2} (ThreadingModel={3})' -f $hive, $view, $key.GetValue(''), $key.GetValue('ThreadingModel')
$key.Dispose()
}
$base.Dispose()
}
}
Those four lines of output are the answer to your triage, as they stand.
- All four say “not registered” → it really is unregistered. Recheck that the registration mechanism matches the classification in 6.2.
- It shows up only under
Registry32→ it is registered only on the 32bit side. A 64bit process will get0x80040154. - It shows up only under
CurrentUser→ only that user can see it. Other users, service accounts, and elevated processes cannot. - The path appears but it still does not work → check whether the file the default value of
InprocServer32points to actually exists, and whether that file’s bitness matches the host.
6.4. Finally, Check Whether Privileges Are Masking the Problem
Lastly, verify whether the problem is genuinely about privileges, or whether privileges merely make a different registration visible.
- Does the behavior change between standard user and administrator
- What changes when you elevate Visual Studio
- Does it reproduce under a service account or a different user
- Does it hold up in a clean environment provisioned via the installer
If you have only ever looked at a single development machine, this is where you truly misjudge things.
7. Operating Decisions That Prevent Breakage If Made Up Front
In COM / ActiveX / OCX development and maintenance, how you decide to operate often matters more than implementation technique.
7.1. Decide the Bitness Policy First
Decide this at the start.
- Are you committing to x86
- Is x64 the source of truth
- Are you supporting both
- Does that particular component have to be in-proc
Especially if a vendor OCX is pinned to x86, ignoring that and porting only your app to x64 will get you stuck later. As a larger architectural topic, A Worked Example of a COM Bridge for Calling a 64-bit DLL from a 32-bit App is also relevant here.
7.2. Decide the Registration Strategy
Registration is also something you should not do ad hoc.
- Used machine-wide → per-machine registration via the installer
- Used only by that user → use per-user deliberately
- Closed within your own app → consider registration-free COM
- Needed only for development → confine it to an explicit dev setup script
Registration-free COM can carry activation information in a manifest instead of the registry, making it an effective way to reduce registration hell. Both the Win32 side of registration-free COM and the .NET side of RegFree COM are officially documented.31632
Mechanically, when COM processes a call such as CoCreateInstance, the order is: look for an activation context first, and fall back to the registry only when there is no information there. A manifest is the file that declares the contents of that activation context.31
You need two manifests, and both go in the same folder as the exe.
The first is the component-side assembly manifest (VendorCtl.manifest). It states which file provides which CLSID.33
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type="win32"
name="MyCompany.VendorCtl"
version="1.0.0.0"
processorArchitecture="x86" />
<file name="vendor.ocx">
<comClass description="Vendor Control"
clsid="{00000000-0000-0000-0000-000000000000}"
threadingModel="Apartment"
progid="Vendor.Control.1" />
</file>
</assembly>
The second is the application-side application manifest (MyApp.exe.manifest). All it states is the dependency on the assembly above.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type="win32"
name="MyCompany.MyApp"
version="1.0.0.0"
processorArchitecture="x86" />
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32"
name="MyCompany.VendorCtl"
version="1.0.0.0"
processorArchitecture="x86" />
</dependentAssembly>
</dependency>
</assembly>
These are the points that are easy to get wrong when writing them.33
- The
assemblyIdentityon the dependent side must match theassemblyIdentityon the component side exactly. If even one ofname,version, orprocessorArchitecturediffers, it will not resolve. - Keep the assembly name and the DLL name distinct. When you place the manifest as a separate file, the assembly name and the manifest name have to differ from the DLL name.
- Element names and attribute names are case-sensitive. Write
comClassasComClassand it will not be read. processorArchitectureis the bitness itself. Anamd64application cannot resolve an x86 OCX. Always keep this aligned with the bitness policy from 7.1.- Embedding an OCX may require the
miscStatusfamily of attributes. The information that corresponds to theMiscStatusregistry key has to be transcribed on the manifest side asmiscStatus,miscStatusContent, and so on.
7.3. Don’t Leave Too Many Artifacts Outside Source Control
A classic failure mode in OCX / ActiveX work is the scattering of dependencies.
- the OCX itself
- dependent DLLs
- the TLB
.licfiles- interop DLLs
- AxHost wrappers
- registration scripts
- a sample host
If these exist only on somebody’s local machine, something will break a few months later - guaranteed.
At a minimum, keep the following next to the code:
- which version is assumed
- what gets installed in what order
- which command performs the registration
- whether it targets x86 or x64
8. Consultations That Fit This Topic Well
For this topic, triage and policy clarification alone tend to deliver value, before any full-scale rework.
For example, it pairs especially well with consultations like:
- You want the causes of
0x80040154or0x80070005sorted into bitness / registration / privileges - The Designer broke after upgrading to Visual Studio 2022 and you want to see how much can be salvaged
- You want to keep a vendor OCX while moving the surrounding code toward .NET and C#
- You want to decide how long to extend x86-pinned assets, and where to bridge / wrap / replace
- You want to stop depending on hand-run
regsvr32and redesign install / deploy
For the keep / wrap / replace decision itself, How to Handle ActiveX / OCX Today - A Keep / Wrap / Replace Decision Table should also be helpful.
9. Summary
When you get stuck in COM component or OCX / ActiveX development, the cause almost always comes down to one of these four:
- The bitness does not match
- The registration method is wrong
- The registration scope (HKCU / HKLM, 32bit / 64bit view) is misaligned
- Something that is only visible thanks to privileges is being mistaken for a healthy state
With Visual Studio 2022 going 64bit, designs that used to “somehow just work” now surface their problems much more readily.12 That is exactly why, when touching COM / OCX / ActiveX, the shortcut is to align the environmental assumptions before writing code.
Rather than counting how many times you run regsvr32, sort out:
- which process is hosting
- what bitness that process is
- where the registration should go
- whether that registration genuinely requires administrator rights
- whether you are looking at the Designer and the runtime separately
— and you will solve it far faster.
References
-
Microsoft Learn, Visual Studio 2022 version 17.0 Release Notes —
devenv.exe is now 64-bit only. ↩ ↩2 ↩3 -
Microsoft Learn, Troubleshoot 32-bit problems - Windows Forms — Visual Studio 2022 is a 64bit process and cannot directly load 32bit .NET / COM / ActiveX components; constraints of the out-of-process designer. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Classes and Servers — COM registration, HKCU / HKCR, self-registration and
DllRegisterServer. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, regsvr32 — syntax and role of
regsvr32. ↩ ↩2 -
Microsoft Learn, Registering Assemblies with COM — COM registration for .NET Framework uses
Regasm.exe. ↩ ↩2 -
Microsoft Learn, Exposing .NET Core components to COM —
EnableComHosting, the generated.comhost.dll,regsvr32,EnableRegFreeCom. ↩ ↩2 ↩3 -
Microsoft Learn, Merged View of HKEY_CLASSES_ROOT — HKCR is a merged view of HKLM and HKCU. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, HKEY_CLASSES_ROOT Key — applications that require administrator rights are advised to register into the per-machine COM configuration. ↩ ↩2
-
Microsoft Learn, COM Error Codes (Generic) (Winerror.h) —
REGDB_E_CLASSNOTREG (0x80040154)and others. ↩ -
Microsoft Learn, You receive 0x80070005 error when you try to register a DLL by using Regsvr32.exe — the typical case of DLL registration failing due to insufficient privileges. ↩ ↩2
-
Microsoft Learn, Registry Redirector — the 32bit / 64bit registry views under WOW64, the fact that the physical location of
HKLM\Softwarefor 32bit code isWow6432Node, and the advice not to touch the physical path directly from an application. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, File System Redirector —
%windir%\System32on x64 Windows and WOW64 file system redirection. ↩ -
Microsoft Learn, Overview of compatibility considerations for 32-bit programs on 64-bit versions of Windows — file / registry redirection under WOW64. ↩
-
Microsoft Learn, Regasm.exe (Assembly Registration Tool) — the role of
Regasm.exeand options such as/tlb. ↩ ↩2 ↩3 -
Microsoft Learn, Packaging a .NET Framework Assembly for COM — type libraries and
Regasm.exe /tlb. ↩ -
Microsoft Learn, Registering the DLL Server for Surrogate Activation — the conditions for sitting in a surrogate, the fact that starting the EXE server or the service takes precedence when
LocalServer/LocalServer32/LocalServiceis present, and the configuration that makes theAppIDthe same GUID as the CLSID. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, DllSurrogate —
DllSurrogateunderAppIDis aREG_SZ; an empty string means the system default surrogate, while writing a path means the custom surrogate at that path is used. ↩ ↩2 -
Microsoft Learn, Using the system-supplied surrogate — specifying an empty string or
NULLstarts the system default surrogate; handling of the threading model and process lifetime inside the surrogate. ↩ ↩2 -
Microsoft Learn, CLSCTX enumeration — several
CLSCTXvalues passed OR’d together are tried in enumeration order, at the in-proc stage theInprocServer32key is used if it exists, and when neither the client nor the server states a bitness preference a server of the bitness matching the client is chosen, with the other bitness started if there is none. ↩ ↩2 ↩3 -
Microsoft Learn, Understanding Custom Build Steps and Build Events — example of using
regsvr32.exein a post-build event. ↩ -
Microsoft Learn, Windows registry for advanced users —
HKCU\Software\ClassesandHKLM\Software\Classes, HKCR behavior. ↩ -
Microsoft Learn, Find, install, and manage extensions for Visual Studio — handling of per-user extensions when running elevated. ↩
-
Microsoft Learn, MFC ActiveX Controls: Licensing an ActiveX Control — design-time / run-time licenses,
.LIC. ↩ -
Microsoft Learn, Application Settings, MFC ActiveX Control Wizard — run-time license generation and the
.licfile. ↩ -
Microsoft Learn, License information for this component not found. You don’t have an appropriate license to use this functionality in the design environment. ↩
-
Microsoft Learn, Aximp.exe (Windows Forms ActiveX Control Importer) — converting ActiveX into a WinForms wrapper. ↩
-
Microsoft Learn, AxHost Class — the AxHost-based wrapper generated by the ActiveX Control Importer. ↩
-
Microsoft Learn, Initializing the COM Library —
CoInitializeEx, per-thread initialization, the STA message loop. ↩ ↩2 ↩3 -
Microsoft Learn, Single-Threaded Apartments — the STA message loop, marshaling,
ThreadingModel. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, Creating Registration-Free COM Objects — registration-free COM via activation contexts. ↩ ↩2
-
Microsoft Learn, Registration-Free COM Interop — registration-free COM interop in .NET Framework. ↩
-
Microsoft Learn, Assembly Manifests — the attributes of
assembly/assemblyIdentity/dependency/file/comClass, the requirement that the REF-side and DEF-sideassemblyIdentitymatch, the handling of name case, and themiscStatusfamily of attributes. ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Why ActiveX Stops Working in Office 2024/Microsoft 365 and How to Diagnose It
When ActiveX stops working in Office 2024/Microsoft 365, work through the causes in order: default disablement, 32-bit/64-bit mismatch, C...
What Are COM / ActiveX / OCX? - The Differences and Relationships Explained
A practical guide to what COM is, what ActiveX is, and what OCX is - covering their differences and relationships, the connection to OLE,...
How to Handle ActiveX / OCX Today - A Keep / Wrap / Replace Decision Table
When you find ActiveX / OCX, how to choose between keeping, wrapping, and replacing it, covering 32-bit / 64-bit, registration, browser d...
A Worked Example of a COM Bridge for Calling a 64-bit DLL from a 32-bit App
When a 32-bit app cannot call a 64-bit DLL directly, a COM bridge can connect them. We walk through the approach, including the Windows c...
Windows App Outsourcing and Custom Software Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or custom software development, here is how to sort out existing software modification, devi...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
ActiveX Migration
Topic page for staged decisions around keeping, wrapping, or replacing COM / ActiveX / OCX assets.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Legacy Asset Reuse & Migration Support
We help plan staged migration while continuing to reuse COM / ActiveX / OCX assets, native code, and 32-bit dependencies.
Frequently Asked Questions
Common questions about the topic of this article.
- How should I investigate 0x80040154 (Class not registered)?
- Officially it is REGDB_E_CLASSNOTREG, literally a class-not-registered error, but it does not always mean the class is completely unregistered. It also happens when the class is registered only in the registry view of the other bitness, or only under HKCU where it is visible to that one user. The fast route is to pin down whether the host process is 32bit or 64bit first, then check that the registration mechanism is the correct one, and then work through where the entry actually landed across HKCU / HKLM and the 32bit / 64bit views.
- Why does it still not work after I register with regsvr32?
- regsvr32 targets native in-proc COM servers (DLL / OCX) that export DllRegisterServer; it is not a magic command that can register anything. Exposing a .NET Framework assembly to COM goes through Regasm.exe, and from .NET 5 onwards you register the .comhost.dll that EnableComHosting generates, using regsvr32. On x64 Windows you also have to pick the right regsvr32 - System32 for the 64-bit side, SysWOW64 for the 32-bit side - because registering on the wrong side leaves the component invisible to the target process even though the command reported success.
- Why does only the Designer break in Visual Studio 2022?
- Because devenv.exe in Visual Studio 2022 is a 64-bit process and cannot directly load 32-bit COM / ActiveX components. The app itself can still run as x86 at runtime, but the Designer runs inside Visual Studio's 64-bit process, which produces the twist where the app is alive at runtime while only the Designer crashes. Switching to AnyCPU does not settle it either, as long as something down the reference chain depends on a COM / ActiveX component pinned to 32-bit.
- Why does it work when I run as administrator but not under normal privileges?
- The real issue is usually not the privileges themselves but a mismatch in registration scope or installation design. COM looks at HKCU\Software\Classes first, and HKEY_CLASSES_ROOT is a merged view of HKLM and HKCU. If developer A registered the component by hand under their own user, it routinely works under A's account while failing for other users and for service accounts. The basics are to distinguish a development per-user registration from the production per-machine registration done by the installer, and to keep build and registration separate.