-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Windows ComHost COM registration in HKCU instead of HKLM? #45750
Comments
I'm aware that registering com servers in the user hive functions but is it a supported configuration? You cite using DllInstall but that simply allows you to integrate custom registration logic it doesn't provide you a supported way to register user rather than local machine level servers. As far as I'm aware registration free com using manifests is the suggested way of dealing with registration elevation permissions requirement issues. |
Supported by who? what? As I said, yes, it's supported by CoCreateInstance, and COM/Windows in general, including all Shell API (Explorer, Common Dialogs, etc.). I know DllInstall in itself doesn't allow anything special, I'm not specifically attached to that, but try Visual Studio ATL's and you'll see that's how it supports HKCU registration in the wizard-generated code:
registration-free means you must distribute the components with the clients. This answers different scenarios. |
At present, it is not supported in .NET Core 3.0+ or .NET 5. COM registration is only supported for the HKLM hive. This was chosen for security reasons. In .NET Framework the behavior had issues that were not appropriate and relied upon legacy registry behavior from the Windows 95/98 days. We are now explicit about this and force HKLM.
Yes. That would be our suggested workaround.
@smourier Your concerns with RegFree COM are valid and I empathize with the issue. I don't personally think we should be continuing some of the questionable COM practices of yore, but do recognize there are applications that have been built up around this option. We can absolutely add support for it, as you discovered it is hardcoded and wouldn't be technically difficult to add a flag or some metadata that indicates where one would like the registration to go. When it was implemented the "most common" scenario was selected - not a lot more thought put into it. Feel free to propose a mechanism in this issue's description or we can prioritize it as is with other interop feature requests. I would imagine a new attribute would suffice or perhaps extend |
Thanks for your answer. If such a feature is added, it's very important to decide where registration happens at deployment time vs compilation time. This is what the ATL-generated code does. So, my initial idea was really not to change any .NET API but instead add support in comhost for a DllInstall parameter that would instruct the registration to happen in HKCU instead of HKLM. So, COM-support related code stays in COM-support related binaries, impact on .NET is minimal. Since .NET 5 requires each COM coclass guid to be specified, I could even imagine a DllInstall command that could be able to support one component in HKCU, another in HKLM, but maybe that's too complicated. By default, all components would be in HKCU or all in HKLM. regsvr32 (which supports DllInstall) is very well known and in fact already referenced in the .NET Core doc here: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.comregisterfunctionattribute?view=net-5.0) but that would support custom installers calling DllInstall too. |
Adding the ability to perform user registrations would also have an effect on tools that generate or gather registration information. For example when I last used WiX to generate installers the official advice was not to try and invoke DllInstall dynamically at install time but to do a gather step and embed the resulting registry alterations. Properly written this won't change if registration if in hkcu instead of hklm but I expect there to be improperly written apps out there that only redirect hklm because that's all that was supported. This is why I asked whether it was a supported scenario, while I know per-user registration works if it isn't supported it can put consumers in a situation where they are doing something that could cause a support ticket to be rejected. For clarity, I like the idea and I think extending |
@AaronRobinsonMSFT You say:
Do you have some more information or a reference about these issue? The registry behaviour under the Office Click-to-Run environment (I think it is related to App-V) has also been problematic in recent Office versions, with undocumented registry types and unexpected behaviour in different versions. In general the component developer has little control over the environment into which this must be installed or run, so so some flexibility about the registration is very valuable - when it runs, registry vs. SxS activation contexts, and some HKCU plan if possible. |
Missed this query. It is entirely possible and one could use the existing source to start. One missing aspect from the source is the need to link against our public low level host API (i.e.
@smourier Yep. Support for COM in .NET Core 3+ and .NET 5 was designed to make the UX align with long standing COM tooling. This may be a difficult change in the short term but we are hoping it aligns better with long standing COM developers as opposed to .NET providing an alternative COM registration methodology. Your suggestion to use I personally have no issue with enabling
@Wraith2 Yes that is indeed the correct advice. WiX and MSIs handle the reference counting logic for the installation whereas
@govert Unfortunately those details aren't something I am permitted to discuss for various reasons. I can say we are now explicitly registering in the HKLM hive to ensure we know where the server is registered. This provides explicit behavior that aligns with work done in the OS during the Windows Vista time frame. I really can't comment further. |
The traditional way to write the COM related registry keys in the HKCU hive on demand is to use the OaEnablePerUserTLibRegistration and RegOverridePredefKey functions. Once you have moved the registry context to HKCU, you would just load the All HKCR related keys are then written to While I've also got some .NET code I could publish (just in case anybody is interested at some point in the future), which does all that and that I have been using for years in scenarios, where Reg-Free-COM via manifests is not an option (e.g. when you want to load components into Office applications, but can't get elevated and of course can't just modify the manifest file of the Office app).
@AaronRobinsonMSFT I would also suggest a new attribute (or attribute pair). I am also interested in that, because of .NET's current lack of TLB generation. So when I manually generate a TLB from an IDL file via MIDL.exe, I want to register the type library during the standard registration process. This is currently only possible by using the |
@lauxjpn I'm not sure I follow this. The function marked with |
@AaronRobinsonMSFT Something like that. It does not necessarily have to be tied to the TLB, but it should be called only once per Maybe an additional attribute can be introduced, that could then be placed on a class, marking it as a A more modern approach would be to use something convention based, but this would break with the pattern to use attributes, that has been established for COM in .NET about 20 years ago, so its probably not a good idea. |
So I could be mistaken here, but a function with that attribute is only called once during the respective
This would be creating a lot of new policy for a system that we don't believe will continue to evolve much. Is there a perception that this approach is something that will grow in popularity which would make investment important or slowly decrease as time progresses? Office - which has historically been the most popular .NET COM interop consumer - seems to focus far more on JavaScript Add-Ins. Note: I am not suggestion COM or Office COM extensions aren't important. I am trying to understand the community perspective and if this is a growing area to invest in or if this area may not warrant time to prioritize improvements in. |
There are two main differences from my point of view here:
On the Win32 level, COM is still the number one choice to implement new features and from a .NET perspective, it is the glue technology to integrate your application/components with any (well, most) non-.NET applications/components on Windows. Since .NET 5 does not provide integrated type library support, manually generating type libraries and registering them will become much more common in the future.
While this is true, Microsoft has introduced many new technologies for their products over the years, while only very few of them usually stick. In regards to Office, there is an over 20 year history of VBS and COM component consumption, so even if JavaScript Add-Ins will become popular of the next few years, there are so many components and applications out there relying on COM, that first-class support will need to be part of .NET on the traditional Windows platform forever anyway. If .NET 5 would have type library support, this would be less of an issue. But since it hasn't, the need to customize your COM classes and application registrations has significantly increased.
I am not sure, that this is really the case. Reusing the same attributes for the same purpose on a different level seems intuitive to me. It would not be a compatibility issue and pretty much in line with how the COM interop implementation handles similar issues. It would also be easy to discover, since the additional usage scenario would be mentioned in the docs for the same attribute that has been used (and misused) for 20 years for similar cases. (I personally am more of a fan of convention based approaches, where |
Fair point. So the idea here would be at the assembly level not specifically tied to an exported COM class. Message received, I agree with your perspective on it being semantically confusing. This argument is convincing given how much work it would be to consider support - limited by our other priorities of course.
We are not entirely in agreement here. The WinRT API surface area is where new APIs are appearing. WinRT is built on top of COM so there is overlap but the system is fundamentally different and registration is different - TLBs don't exist. In addition to that the C#/WinRT repo is working to provide a .NET component authoring story. I would argue that is where we should invest rather than the built-in COM system.
I fully agree with respect to .NET Framework and that is also our statement as it relates to COM interop. However, for .NET 5+ this is not a specific goal as it relates to the Office ecosystem. There are many issues with the Office COM model and .NET 5+ and it is not clear if Office will fully support .NET 5+ until multiple versions of the runtime can be loaded in the same process - which is not a goal at present - see #45285 (comment). I think until Office reprioritizes the COM extension approach or another scenario pushes us toward enabling multiple in-proc runtime instances the best advice here is to focus on .NET Framework development for Office extensions. |
Yeah, that's it.
That's definitely the case for APIs in general. (I was really only talking about the Win32 layer here.)
I agree that for the time being, writing .NET Standard 2.0 compatible net48 code is probably the best choice for Office compatible COM classes. |
I want to add my support / vote for this request. We usually prefer using HKCU over HKLM since the addins we provide for use with Office usually are scoped for users and needs to be available to corporate uses who may have a locked down environment and thus cannot write to HKCU. Manifest approach is not practical because that requires creating a manifest associated with an .exe file for the Office which will require admin privilege anyway AND it is also improper because it makes the scope is to the all documents managed by the Office when in fact it may only apply to certain kind of documents (e.g. a COM addin/library is used by only one specific Excel spreadsheet or a particular Access database). The workaround is to manually write a |
@bclothier The general way to accomplish what you want, is to register your in-proc DLL after calling RegOverridePredefKey. For type library registrations, also call OaEnablePerUserTLibRegistration. (If necessary, I can probably share some C# code I have been using for about 10 years to register COM libraries under HKCU, so they can be registered and loaded from a normal user context without elevation. I have used it also for COM and ActiveX components that needed to be loaded in Excel or Access). |
@lauxjpn Thank you for the insight! This is very good to know but I'm a bit fuzzy on one point. Given that we call the |
@bclothier Not sure what the The general procedure would look like this:
|
Sorry, I mistyped. I meant Given that |
The The procedure I outlined does not depend on How you execute the procedure is really up to you and depends on how you run and deploy your app or components. |
If you are looking for a way to generate a TLB from a .NET 5+ assembly, this project might help you: |
@marklechtermann This is a very interesting project! Thank you for sharing and providing a community solution for TLB tooling in .NET 5+. |
Building a TLB from .NET 5+ is not the subject of this issue, but anyway, IMHO, this should be provided by Microsoft like it used to be, not by the community. |
@lauxjpn I would be very interested in seeing this code! |
@gusmally Here is the COM library registration code that I have been using for a long time. It is able to register libraries for the current user: COM library registration source codeusing System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Registration
{
public static class ComLibrary
{
#region External Declarations
[DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FreeLibrary(IntPtr hModule);
#endregion
private delegate int ComLibraryFunctionDelegate();
private delegate void EnableTypeLibRegistrationForCurrentUserDelegate();
private static bool IsTypeLibRegistrationForCurrentUserEnabled { get; set; }
public static bool Register(string libraryPath, bool currentUserOnly = false, TraceSource traceSource = null)
{
traceSource?.TraceInformation($"COM library \"{libraryPath}\" is being registered for {(currentUserOnly ? "the current user" : "all users")}.");
var succeeded = false;
if (currentUserOnly)
{
EnableTypeLibRegistrationForCurrentUser(
() =>
{
using (new UserClassesRegistryContext())
succeeded = CallLibraryFunction("DllRegisterServer", libraryPath, traceSource);
}, traceSource);
}
else
succeeded = CallLibraryFunction("DllRegisterServer", libraryPath, traceSource);
traceSource?.TraceInformation($"The registration process {(succeeded ? "succeeded" : "failed")}.");
return succeeded;
}
public static bool Unregister(string libraryPath, bool currentUserOnly = false, TraceSource traceSource = null)
{
traceSource?.TraceInformation($"COM library \"{libraryPath}\" is being unregistered for {(currentUserOnly ? "the current user" : "all users")}.");
var succeeded = false;
if (currentUserOnly)
{
EnableTypeLibRegistrationForCurrentUser(
() =>
{
using (new UserClassesRegistryContext())
succeeded = CallLibraryFunction("DllUnregisterServer", libraryPath, traceSource);
}, traceSource);
}
else
succeeded = CallLibraryFunction("DllUnregisterServer", libraryPath, traceSource);
traceSource?.TraceInformation($"The unregistration process {(succeeded ? "succeeded" : "failed")}.");
return succeeded;
}
private static void EnableTypeLibRegistrationForCurrentUser(Action action, TraceSource traceSource = null)
{
if (IsTypeLibRegistrationForCurrentUserEnabled)
{
action();
return;
}
traceSource?.TraceInformation("User specific type library registration will be used.");
var hModule = IntPtr.Zero;
const string libraryPath = "Oleaut32.dll";
const string functionName = "OaEnablePerUserTLibRegistration";
try
{
hModule = LoadLibrary(libraryPath);
if (hModule == IntPtr.Zero)
throw new InvalidOperationException($"The library \"{libraryPath}\" could not be loaded.");
var libraryFunctionProcAddress = GetProcAddress(hModule, functionName);
if (libraryFunctionProcAddress != IntPtr.Zero)
{
var libraryFunction = (EnableTypeLibRegistrationForCurrentUserDelegate)Marshal.GetDelegateForFunctionPointer(libraryFunctionProcAddress, typeof(EnableTypeLibRegistrationForCurrentUserDelegate));
libraryFunction();
}
IsTypeLibRegistrationForCurrentUserEnabled = true;
action();
}
finally
{
IsTypeLibRegistrationForCurrentUserEnabled = false;
if (hModule != IntPtr.Zero)
{
FreeLibrary(hModule);
hModule = IntPtr.Zero;
}
}
}
private static bool CallLibraryFunction(string functionName, string libraryPath, TraceSource traceSource = null)
{
var hModule = IntPtr.Zero;
try
{
hModule = LoadLibrary(libraryPath);
if (hModule == IntPtr.Zero)
throw new InvalidOperationException($"The library \"{libraryPath}\" could not be loaded.");
var libraryFunctionProcAddress = GetProcAddress(hModule, functionName);
if (libraryFunctionProcAddress == IntPtr.Zero)
throw new InvalidOperationException($"The entry point \"{functionName}\" of COM library \"{libraryPath}\" could not be found.");
var libraryFunction = (ComLibraryFunctionDelegate)Marshal.GetDelegateForFunctionPointer(libraryFunctionProcAddress, typeof(ComLibraryFunctionDelegate));
var hResult = libraryFunction();
traceSource?.TraceInformation($"The entry point \"{functionName}\" of COM library \"{libraryPath}\" returned an HRESULT value of \"{hResult}\".");
return hResult == 0;
}
finally
{
if (hModule != IntPtr.Zero)
{
FreeLibrary(hModule);
hModule = IntPtr.Zero;
}
}
}
}
public class UserClassesRegistryContext : IDisposable
{
#region External Declarations
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int RegOpenKey(IntPtr hKey, string lpSubKey, out IntPtr phkResult);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int RegOverridePredefKey(IntPtr hkey, IntPtr hnewKey);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int RegCloseKey(IntPtr hKey);
#endregion
private const long ERROR_SUCCESS = 0;
public bool InContext { get; private set; }
public UserClassesRegistryContext()
{
EnterContext();
}
~UserClassesRegistryContext()
{
ReleaseUnmanagedResources();
}
public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
private void ReleaseUnmanagedResources()
{
if (InContext)
ExitContext();
}
private void EnterContext()
{
if (RegOpenKey(new IntPtr((int) RegistryHive.CurrentUser), @"Software\Classes", out var hKey) != ERROR_SUCCESS)
throw new InvalidOperationException(@"The registry key ""HKEY_CURRENT_USER\Software\Classes"" could not be opened.");
try
{
if (RegOverridePredefKey(new IntPtr((int) RegistryHive.ClassesRoot), hKey) != ERROR_SUCCESS)
throw new InvalidOperationException(@"The registry key ""HKEY_CLASSES_ROOT"" could not be overridden by ""HKEY_CURRENT_USER\Software\Classes"".");
InContext = true;
}
finally
{
RegCloseKey(hKey);
}
}
private void ExitContext()
{
if (!InContext)
throw new InvalidOperationException("ExitContext was called before calling EnterContext first.");
RegOverridePredefKey(new IntPtr((int)RegistryHive.ClassesRoot), IntPtr.Zero);
InContext = false;
}
}
} Just call |
As far as I know, currently, to register a COM object written in .NET 5 (or .NET Core 3.x for that matter) we need to
"Open an elevated command prompt and run regsvr32 ProjectName.comhost.dll. That will register all of your exposed .NET objects with COM." from here
https://docs.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com#register-the-com-host-for-com
I think the relevant source is here: https://github.com/dotnet/runtime/blob/master/src/installer/corehost/cli/comhost/comhost.cpp#L309 it seems pretty much hardcoded for HKEY_LOCAL_MACHINE.
As you know, registering COM component in HKCU (HKEY_CURRENT_USER\SOFTWARE\Classes\CLSID for the CLSID, etc.) is pretty common and supported by "COM" in general (CoCreateInstance, etc.). It's also supported by tooling such as Visual Studio's ATL and Windows' regsvr32.exe with the help of DllInstall (optional) DLL export:
https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-dllinstall
It has the enormous advantage of avoiding elevated rights for setup, installers, etc.
Can support for this be added to comhost?
In the meantime, is there any way to do this with current .NET 5? Should I write a custom comhost? How?
The text was updated successfully, but these errors were encountered: