Closed cheverdyukv closed 1 day ago
API proposal issue: https://github.com/dotnet/runtime/issues/42275
Is there any progress being made? I'm currently working on a Mod Manager for FF7 on PC ( https://github.com/tsunamods-codes/7th-Heaven ) and I've been impacted exactly by this. By hooking two APIs ( CloseHandle and DuplicateHandle ) using the Hostxfr, which do throw unhandled exceptions if the handle is invalid, makes the game crash. Having the possibility to ignore unhandled exceptions or handle them instead of force closing the process would be HIGHLY appreciated.
Thank you in advance.
You can use GetCLRRuntimeHost to initialize runtime second time and disable unhanded exceptions. I didn't full battle test it, but our huge app starts fine with it. I also test it with unhanded exception and it suppresses them.
It just must be at appropriate time.
@vladimir-cheverdyuk-altium I'd be very interested to test this. Do you have a sample code I can use as a basis? Thanks!
I have it but it in Delphi (Pascal like language). If you want I can paste a few functions here.
Yep, fine by me. I can relate, thanks :)
// hostfxr does not have ability to suppress unhandled exception. See https://github.com/dotnet/runtime/issues/39587
// As result I had to use obsolete way to do it.
// This function suppose to do nothing except setting up APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_IGNORE_UNHANDLED_EXCEPTIONS
// Unfortunatelly we cannot use this way to initialize .NET runtime because this API do not initialize AssemblyLoadContext
// For more info check https://github.com/dotnet/runtime/issues/39167
Procedure DisableUnhandledExceptions;
Const
cDefaultAppDomainId = 1; // There is only one domain in .NET Core
Type
TGetCLRRuntimeHostFunc = Function(Const riid : TGUID) : IUnknown; Safecall;
Var
ClrDirectory : String;
hCoreClr : THandle;
GetCLRRuntimeHostFunc : TGetCLRRuntimeHostFunc;
Host : ICLRRuntimeHost2;
DomainId : DWORD;
PropCount : SIZE_T;
PropKeys : Array Of pchar_t;
PropValues : Array Of pchar_t;
ErrorCode : Int32;
Begin
ClrDirectory := ExtractFilePath(GetRuntimeProperty('FX_DEPS_FILE'));
hCoreClr := LoadLibraryEx(PChar(ClrDirectory + 'coreclr.dll'), 0, 0);
If hCoreClr = 0 Then
RaiseLastOSError(GetLastError, 'Failed to load CoreCLR');
GetCLRRuntimeHostFunc := GetProcAddress(hCoreClr, 'GetCLRRuntimeHost');
If Not Assigned(GetCLRRuntimeHostFunc) Then
Raise Exception.Create('Cannot find "GetCLRRuntimeHost" function');
Host := ICLRRuntimeHost2(GetCLRRuntimeHostFunc(ICLRRuntimeHost2));
If Host = Nil Then
Raise Exception.Create('Failed to create ICLRRuntimeHost2');
// Host already initialized, so there is no need to do it again
// Host.SetStartupFlags(STARTUP_FLAGS.STARTUP_CONCURRENT_GC Or STARTUP_FLAGS.STARTUP_SINGLE_APPDOMAIN Or STARTUP_FLAGS.STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN);
Host.Start();
PropCount := 0;
// Get number of properties
ErrorCode := gv_hostfxr_get_runtime_properties_fn(gv_HostfxrHandle, PropCount, Nil, Nil);
If (ErrorCode <> StatusCode.Success) And (ErrorCode <> Int32(StatusCode.HostApiBufferTooSmall)) Then
Raise Exception.Create('Failed to call hostfxr_get_runtime_properties');
// Allocate arrays for names and values
SetLength(PropKeys, PropCount);
SetLength(PropValues, PropCount);
// Retrieve names and values
If gv_hostfxr_get_runtime_properties_fn(gv_HostfxrHandle, PropCount, @PropKeys[0], @PropValues[0]) <> StatusCode.Success Then
Raise Exception.Create('Failed to call hostfxr_get_runtime_properties');
// There is only one domain possible to .NET Core, so we can create it here
// We pass all properties from hostfxr because otherwise they will be empty
DomainId := Host.CreateAppDomainWithManager(
'Default Domain',
APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS Or
APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP Or
APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_DISABLE_TRANSPARENCY_ENFORCEMENT Or
APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_IGNORE_UNHANDLED_EXCEPTIONS,
Nil,
Nil,
PropCount,
@PropKeys[0],
@PropValues[0]);
// Just o check that it didn't fail
If DomainId <> cDefaultAppDomainId Then
// Only one domain can be created in .NET. Retriving default domain is not thread safe and as a result we will use constant
Raise Exception.Create('Invalid domain id');
End;
Procedure InitializeHostFxr(HostPath, DotNetRoot, NativeImagesPaths, TrustedPlatformAssemblies : String);
Var
HostDirectory : String;
ErrorCode : Integer;
HostfxrHandle : THandle;
hostfxr_initialize_for_runtime_config_fn : hostfxr_initialize_for_runtime_config_type;
hostfxr_get_runtime_delegate_fn : hostfxr_get_runtime_delegate_type;
InitializePameters : hostfxr_initialize_parameters;
Begin
HostDirectory := ExtractFileDir(HostPath);
HostfxrHandle := LoadLibraryEx(PChar(HostDirectory + '\hostfxr.dll'), 0, 0);
If HostfxrHandle = 0 Then
RaiseLastOSError(GetLastError, 'Failed to load CoreCLR');
hostfxr_initialize_for_runtime_config_fn := FindProcAddress(HostfxrHandle, 'hostfxr_initialize_for_runtime_config');
hostfxr_get_runtime_delegate_fn := FindProcAddress(HostfxrHandle, 'hostfxr_get_runtime_delegate');
gv_hostfxr_get_runtime_property_value_fn := FindProcAddress(HostfxrHandle, 'hostfxr_get_runtime_property_value');
gv_hostfxr_set_runtime_property_value_fn := FindProcAddress(HostfxrHandle, 'hostfxr_set_runtime_property_value');
gv_hostfxr_get_runtime_properties_fn := FindProcAddress(HostfxrHandle, 'hostfxr_get_runtime_properties');
InitializePameters.size := SizeOf(InitializePameters);
InitializePameters.host_path := pchar_t(HostPath);
InitializePameters.dotnet_root := pchar_t(DotNetRoot);
ErrorCode := hostfxr_initialize_for_runtime_config_fn(pchar_t(ChangeFileExt(HostPath, '.runtimeconfig.json')), InitializePameters, gv_HostfxrHandle);
If ErrorCode <> StatusCode.Success Then
Raise Exception.CreateFmt('hostfxr_initialize_for_runtime_config returned error code %x', [ErrorCode]);
// PROBING_DIRECTORIES will be populated from additionalProbingPaths but it will resolve relative paths based on current directory instead of directory that contains
// application's .runtimeconfig.json file. As result I created new section called additionalAssemblyPaths in .runtimeconfig.json.
// Function ReadAdditionalAssemblyPaths will read .runtimeconfig.json file, parse it and resolve relative papths manualy.
SetRuntimeProperty('APP_PATHS', ReadAdditionalAssemblyPaths(ChangeFileExt(HostPath, '.runtimeconfig.json')));
SetRuntimeProperty('APP_NI_PATHS', NativeImagesPaths);
If TrustedPlatformAssemblies <> '' Then
Begin
// CreateDotNetDelegate without Assembly name parameter works only with types and classes from trusted platform assemblies. This is way to add assembly to that list
// TRUSTED_PLATFORM_ASSEMBLIES will populate automatically during call to hostfxr_initialize_for_runtime_config_fn
// More info: https://docs.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing
SetRuntimeProperty('TRUSTED_PLATFORM_ASSEMBLIES', GetRuntimeProperty('TRUSTED_PLATFORM_ASSEMBLIES') + ';' + TrustedPlatformAssemblies);
End;
// Required for AppContext.BaseDirectory property to work
SetRuntimeProperty('APP_CONTEXT_BASE_DIRECTORY', HostDirectory);
ErrorCode := hostfxr_get_runtime_delegate_fn(gv_HostfxrHandle, hostfxr_delegate_type.hdt_get_function_pointer, @gv_hostfxr_get_function_pointer_fn);
If ErrorCode <> StatusCode.Success Then
Raise Exception.CreateFmt('hostfxr_get_runtime_delegate returned error code %x', [ErrorCode]);
ErrorCode := hostfxr_get_runtime_delegate_fn(gv_HostfxrHandle, hostfxr_delegate_type.hdt_load_assembly_and_get_function_pointer, @gv_hostfxr_load_assembly_and_get_function_pointer_fn);
If ErrorCode <> StatusCode.Success Then
Raise Exception.CreateFmt('hostfxr_get_runtime_delegate returned error code %x', [ErrorCode]);
DisableUnhandledExceptions;
// Just in case, we not suppose to finilize it because our application keeps .NET forever
End;
Amazing! Thanks a lot @vladimir-cheverdyuk-altium very interesting approach, I'll try to translate this into C++ native code and I'll post here the results ASAP I'll be able to test this. Cheers!
Great!
Just in case this code is assume that framework will be packaged with application and there will be .runtimeconfig.json file that application used for own needs in different than intended way. I just pasted that code to demonstrate where you need to call DisableUnhandledExceptions.
Yup, crystal clear how's the approach, I never thought about "abusing it" this way, pretty smart move. It might solve also my own issues. Thanks again, appreciated!
Allright for anyone interested I translated this into C++, unfortunately doesn't fix my issue but the code works as intended:
#include <stdio.h>
#include "mscoree.h" // https://github.com/dotnet/coreclr/blob/master/src/pal/prebuilt/inc/mscoree.h
hostfxr_handle context = nullptr; // here lives the hostfxr context from hostfxr_initialize_for_runtime_config()
static const wchar_t* coreCLRInstallDirectory = L"%programfiles%\\dotnet\\shared\\Microsoft.NETCore.App\\7.0.0";
static const wchar_t* coreCLRDll = L"coreclr.dll";
// -------------------------------
// Check for CoreCLR.dll in a given path and load it, if possible
HMODULE LoadCoreCLR(const wchar_t* directoryPath)
{
wchar_t coreDllPath[MAX_PATH];
wcscpy_s(coreDllPath, MAX_PATH, directoryPath);
wcscat_s(coreDllPath, MAX_PATH, L"\\");
wcscat_s(coreDllPath, MAX_PATH, coreCLRDll);
return LoadLibraryExW(coreDllPath, NULL, 0);
}
void DisableUnhandledExceptions()
{
wchar_t coreRoot[MAX_PATH];
::ExpandEnvironmentStringsW(coreCLRInstallDirectory, coreRoot, MAX_PATH);
HMODULE coreCLRModule = LoadCoreCLR(coreRoot);
ICLRRuntimeHost2* runtimeHost;
FnGetCLRRuntimeHost pfnGetCLRRuntimeHost =
(FnGetCLRRuntimeHost)::GetProcAddress(coreCLRModule, "GetCLRRuntimeHost");
// Get the hosting interface
HRESULT hr = pfnGetCLRRuntimeHost(IID_ICLRRuntimeHost2, (IUnknown**)&runtimeHost);
runtimeHost->Start();
size_t num_props = 0;
DWORD domainId;
hostfxr_get_runtime_properties(context, &num_props, nullptr, nullptr);
const char_t** fxr_keys = new const char_t * [num_props];
const char_t** fxr_values = new const char_t * [num_props];
hostfxr_get_runtime_properties(context, &num_props, fxr_keys, fxr_values);
// Create the AppDomain
hr = runtimeHost->CreateAppDomainWithManager(
L"Default Domain",
APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS ||
APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP ||
APPDOMAIN_DISABLE_TRANSPARENCY_ENFORCEMENT ||
APPDOMAIN_IGNORE_UNHANDLED_EXCEPTIONS,
NULL,
NULL,
num_props,
fxr_keys,
fxr_values,
&domainId
);
}
// Just call DisableUnhandledExceptions() after hostfxr_close();
Main differences from the above version:
coreclr.dll
from the latest .NET 7 frameworkFeel free to tweak at your own heart and desire. Thanks again @vladimir-cheverdyuk-altium for the snippet on your side!
Does it suppress unhanded exceptions?
Please check https://github.com/dotnet/runtime/issues/101560 and let us know if it addresses your scenario.
Hi @jkotas. It looks like what we want to. Thank you!
I'm going to close this against https://github.com/dotnet/runtime/issues/101560, as that seems to address the requested scenarios here.
I was using https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting#create-a-host-using-mscoreeh to host .NET runtime. But after discussion in #39167 I was told to use hostfxr to host .NET runtime.
But as far as I can see there is no way to pass APPDOMAIN_IGNORE_UNHANDLED_EXCEPTIONS and our application needs for 2 reasons:
This is last issue that prevents us to use hostfxr