Registration and Bitness Pitfalls in COM/OCX/ActiveX Development

· Updated: · · 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.

  1. COM / OCX / ActiveX trouble is more often caused by mismatches in bitness (32bit / 64bit), registration location, host, and privileges than by code logic.
  2. Visual Studio 2022 is a 64bit process, so design-time integration that assumed 32bit and used to just work will break as is.12
  3. regsvr32 is 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 use Regasm.exe, and for .NET 5+ / .NET 6+ / .NET 8+ you register the generated .comhost.dll.3456
  4. “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.

COM and ActiveX registration pitfallsDiagram 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 loopusesusesincompatible withrequiresimplementsusesstored inusesrecommended fornot recommended forrecommended forusesrequiresmay causemay causeinherits the contents ofinherits the contents ofrequiresrequiresrequiresusesimplementsrequiresrequirespreventsrequiresusesmitigatesrequiresrequiresusesstored inrequiresusesCOM/OCX Registration PitfallsVisual Studio 2022WOW64 Registry RedirectorActiveXOCXBitness Match RequirementCOM (Component Object Model)HKEY_CURRENT_USER\Software\ClassesCLSID (Class ID)WOW6432Noderegsvr32In-Proc COM (DLL Server)Regasm.exe.NET Framework.NET 5+ COM Hosting (.comhost.dll).NET (Core and Later)WOW64 File System RedirectorAdministrator Privileges0x80040154 (Class Not Registered)Per-User COM RegistrationHKEY_CLASSES_ROOT (HKCR)HKEY_LOCAL_MACHINE\Software\ClassesPer-Machine COM RegistrationActiveX Design-Time and Run-Time LicensesAxImp (ActiveX Control Importer)Type Library (TLB)AxHostCOM apartment model (STA/MTA)Message LoopCoInitializeExSTA Thread Blocking DeadlockMarshalingRegistration-Free COMAssembly Manifest (Reg-Free COM)COM LocalServer (out-of-process server)ProgID (Programmatic Identifier)

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:

  • regsvr32 succeeded
  • 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 /codebase records 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 /codebase be strong-named.
  • /regfile cannot be combined with /unregister or /tlb. What /regfile writes out is only the entries for the managed classes; the TypeLibID and InterfaceID are not emitted. If you assume “shipping the .reg is all the registration you need”, it will fall short.

Mixing these up produces a common pattern:

  • run regsvr32 against a managed DLL
  • naturally, DllRegisterServer is 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 regsvr32 succeeded, a 64bit process cannot load a 32bit InprocServer32. 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 AppID to the CLSID and write an empty-string DllSurrogate into that AppID key.16
  • What starts then is a dllhost.exe of the bitness of the DLL that InprocServer32 points 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.

Whether activation lands on in-proc or on the surrogateA 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.YesNoYesNoYesNoA client requests activationRequested with a CLSCTX that includes in-procDoes the CLSID in the same view have InprocServer32Loaded in-proc as beforeIs there an EXE server registrationThe EXE starts before the surrogateIs there an empty DllSurrogateA dllhost of the DLL side bitness startsCannot 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

  1. The CLSID key has an AppID value, and the corresponding AppID key exists
  2. CLSCTX_LOCAL_SERVER is set in the activation call, and the CLSID key does not have LocalServer32 / LocalServer / LocalService
  3. The CLSID key has InprocServer32
  4. The DLL that InprocServer32 points to actually exists
  5. There is a DllSurrogate value under the AppID key

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 the AppID value is attached on the CLSID side of the 32bit view, and whether the DLL that InprocServer32 points to actually exists
  • dllhost.exe starts but the call dies → check the marshaling preconditions (IDispatch / proxy-stub / the standard marshaler)
  • A different EXE starts → LocalServer32 or 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:

  1. the original OCX / ActiveX itself
  2. the type library
  3. the generated interop / wrapper
  4. 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.

OCX / ActiveXSTA threadMessage queue of the hidden windowCaller on another threadOCX / ActiveXSTA threadMessage queue of the hidden windowCaller on another threadIf the message loop is stoppedthis pull never happens and the caller keeps waitingThe method call is posted as a messageThe message loop pulls it offThe window procedure invokes the matching methodThe return value comes back

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.Run or 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\Classes
  • HKLM\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 get 0x80040154.
  • 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 InprocServer32 points 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 assemblyIdentity on the dependent side must match the assemblyIdentity on the component side exactly. If even one of name, version, or processorArchitecture differs, 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 comClass as ComClass and it will not be read.
  • processorArchitecture is the bitness itself. An amd64 application cannot resolve an x86 OCX. Always keep this aligned with the bitness policy from 7.1.
  • Embedding an OCX may require the miscStatus family of attributes. The information that corresponds to the MiscStatus registry key has to be transcribed on the manifest side as miscStatus, 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
  • .lic files
  • 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 0x80040154 or 0x80070005 sorted 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 regsvr32 and 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:

  1. The bitness does not match
  2. The registration method is wrong
  3. The registration scope (HKCU / HKLM, 32bit / 64bit view) is misaligned
  4. 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

  1. Microsoft Learn, Visual Studio 2022 version 17.0 Release Notesdevenv.exe is now 64-bit only 2 3

  2. 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

  3. Microsoft Learn, Classes and Servers — COM registration, HKCU / HKCR, self-registration and DllRegisterServer 2 3 4

  4. Microsoft Learn, regsvr32 — syntax and role of regsvr32 2

  5. Microsoft Learn, Registering Assemblies with COM — COM registration for .NET Framework uses Regasm.exe 2

  6. Microsoft Learn, Exposing .NET Core components to COMEnableComHosting, the generated .comhost.dll, regsvr32, EnableRegFreeCom 2 3

  7. Microsoft Learn, Merged View of HKEY_CLASSES_ROOT — HKCR is a merged view of HKLM and HKCU.  2 3 4 5

  8. Microsoft Learn, HKEY_CLASSES_ROOT Key — applications that require administrator rights are advised to register into the per-machine COM configuration.  2

  9. Microsoft Learn, COM Error Codes (Generic) (Winerror.h)REGDB_E_CLASSNOTREG (0x80040154) and others. 

  10. 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

  11. Microsoft Learn, Registry Redirector — the 32bit / 64bit registry views under WOW64, the fact that the physical location of HKLM\Software for 32bit code is Wow6432Node, and the advice not to touch the physical path directly from an application.  2 3 4 5

  12. Microsoft Learn, File System Redirector%windir%\System32 on x64 Windows and WOW64 file system redirection. 

  13. Microsoft Learn, Overview of compatibility considerations for 32-bit programs on 64-bit versions of Windows — file / registry redirection under WOW64. 

  14. Microsoft Learn, Regasm.exe (Assembly Registration Tool) — the role of Regasm.exe and options such as /tlb 2 3

  15. Microsoft Learn, Packaging a .NET Framework Assembly for COM — type libraries and Regasm.exe /tlb

  16. 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 / LocalService is present, and the configuration that makes the AppID the same GUID as the CLSID.  2 3 4

  17. Microsoft Learn, DllSurrogateDllSurrogate under AppID is a REG_SZ; an empty string means the system default surrogate, while writing a path means the custom surrogate at that path is used.  2

  18. Microsoft Learn, Using the system-supplied surrogate — specifying an empty string or NULL starts the system default surrogate; handling of the threading model and process lifetime inside the surrogate.  2

  19. Microsoft Learn, Registry Keys Affected by WOW64HKLM\SOFTWARE\Classes\CLSID and HKCU\SOFTWARE\Classes\CLSID are subject to redirection, HKLM\SOFTWARE\Classes\AppID is shared from Windows 7 / Windows Server 2008 R2 onwards, and HKCR is the merged view of the two.  2 3

  20. Microsoft Learn, CLSCTX enumeration — several CLSCTX values passed OR’d together are tried in enumeration order, at the in-proc stage the InprocServer32 key 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

  21. Microsoft Learn, Understanding Custom Build Steps and Build Events — example of using regsvr32.exe in a post-build event. 

  22. Microsoft Learn, Windows registry for advanced usersHKCU\Software\Classes and HKLM\Software\Classes, HKCR behavior. 

  23. Microsoft Learn, Find, install, and manage extensions for Visual Studio — handling of per-user extensions when running elevated. 

  24. Microsoft Learn, MFC ActiveX Controls: Licensing an ActiveX Control — design-time / run-time licenses, .LIC

  25. Microsoft Learn, Application Settings, MFC ActiveX Control Wizard — run-time license generation and the .lic file. 

  26. Microsoft Learn, License information for this component not found. You don’t have an appropriate license to use this functionality in the design environment. 

  27. Microsoft Learn, Aximp.exe (Windows Forms ActiveX Control Importer) — converting ActiveX into a WinForms wrapper. 

  28. Microsoft Learn, AxHost Class — the AxHost-based wrapper generated by the ActiveX Control Importer. 

  29. Microsoft Learn, Initializing the COM LibraryCoInitializeEx, per-thread initialization, the STA message loop.  2 3

  30. Microsoft Learn, Single-Threaded Apartments — the STA message loop, marshaling, ThreadingModel 2 3 4 5

  31. Microsoft Learn, Creating Registration-Free COM Objects — registration-free COM via activation contexts.  2

  32. Microsoft Learn, Registration-Free COM Interop — registration-free COM interop in .NET Framework. 

  33. Microsoft Learn, Assembly Manifests — the attributes of assembly / assemblyIdentity / dependency / file / comClass, the requirement that the REF-side and DEF-side assemblyIdentity match, the handling of name case, and the miscStatus family of attributes.  2

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

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.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog