Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/design/features/host-runtime-information.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,22 @@ Hex string representation of a pointer to a [`host_runtime_contract` struct](/sr

The `get_runtime_property` function provides key-value string information (like that provided for runtime initialization). This removes the requirement to pre-compute and store all properties. [Existing properties](#well-known-runtime-properties) can be migrated to go through this mechanism, allowing for pay-for-play properties and reducing the cost of properties.

Some existing properties (for example, [probing path properties](#probing-paths)) would benefit from being structured data rather than a single string. The contract can be extended to allow querying for that specific, structured information rather than relying only on strings. For backwards compatibility for existing properties, we would still allow getting the string via `get_runtime_property`, but that would be an on-demand cost.
Some existing properties benefit from being provided as structured data rather than a single string. The contract can expose callbacks for querying this information while continuing to provide the corresponding string through `get_runtime_property` for backwards compatibility.

### Application assembly paths

**Added in .NET 12**

The host provides resolved assembly paths through two callbacks:

- `get_assembly_names` returns the simple names of all host-resolved assemblies.
- `resolve_assembly_to_path` resolves a simple name to directory and file name components.

The returned strings are owned by the host and remain valid for the lifetime of the process. The runtime records the assembly names during initialization and resolves each path on demand.

Both callbacks must be available for the structured representation to be used. If either callback is unavailable or `get_assembly_names` returns `false`, the runtime falls back to the [`TRUSTED_PLATFORM_ASSEMBLIES`](#probing-paths) property.

For compatibility, `get_runtime_property` returns the path-separated `TRUSTED_PLATFORM_ASSEMBLIES` string on demand. A custom host can explicitly set that property to use the legacy representation instead of the structured callbacks.

## Well-known runtime properties

Expand Down Expand Up @@ -66,6 +81,8 @@ List of assemblies (paths or names) containing a [`StartupHook`](./host-startup-

List of platform and application assembly file paths. Paths are delimited by a [platform-specific path separator](#path-separator). This is used in [managed assembly probing](https://learn.microsoft.com/dotnet/core/dependency-loading/default-probing#managed-assembly-default-probing).

**.NET 12 and above** The host provides this information through the [application assembly callbacks](#application-assembly-paths) by default. The string representation remains available through `host_runtime_contract.get_runtime_property` and can still be explicitly supplied by a custom host.

`NATIVE_DLL_SEARCH_DIRECTORIES`

List of directory paths to search for unmanaged (native) libraries. Paths are delimited by a [platform-specific path separator](#path-separator). This is used in [unmanaged (native) assembly probing](https://learn.microsoft.com/dotnet/core/dependency-loading/default-probing#unmanaged-native-library-probing).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ private static bool IsKnownHostProperty(string name)
or "PLATFORM_RESOURCE_ROOTS"
or "APP_PATHS";

private static bool TryGetHostPropertyValue(string name, out string? value)
{
value = null;
return TryGetHostPropertyValue(name, new StringHandleOnStack(ref value));
}

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AppContext_TryGetHostPropertyValue", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool TryGetHostPropertyValue(string name, StringHandleOnStack retValue);
Expand Down
79 changes: 48 additions & 31 deletions src/coreclr/binder/applicationcontext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "assemblyhashtraits.hpp"
#include "stringarraylist.h"
#include "failurecache.hpp"
#include "hostinformation.h"
#include "utils.hpp"
#include "ex.h"
#include "clr/fs/path.h"
Expand Down Expand Up @@ -91,51 +92,67 @@ namespace BINDER_SPACE
//
m_pTrustedPlatformAssemblyMap = new SimpleNameToFileNameMap();

sTrustedPlatformAssemblies.Normalize();
for (SString::Iterator i = sTrustedPlatformAssemblies.Begin(); i != sTrustedPlatformAssemblies.End(); )
const char* const* assemblyNames;
size_t assemblyCount;
if (HostInformation::GetAssemblyNames(&assemblyNames, &assemblyCount))
{
SString fileName;
SString simpleName;
HRESULT pathResult = S_OK;
IF_FAIL_GO(pathResult = GetNextTPAPath(sTrustedPlatformAssemblies, i, /*dllOnly*/ false, fileName, simpleName));
if (pathResult == S_FALSE)
for (size_t i = 0; i < assemblyCount; i++)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it save even more memory allocations and copying if we stopped building the hashtable here and asked the host to resolve the assembly name to a filepath on demand every time?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. But maybe still keep the names only? The runtime does a case-insensitive hash for the names, which I don't know that we want to make any host's responsibility.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable

{
break;
}
StackSString simpleName(SString::Utf8, assemblyNames[i]);
_ASSERT(!simpleName.IsEmpty());

const SimpleNameToFileNameMapEntry *pExistingEntry = m_pTrustedPlatformAssemblyMap->LookupPtr(simpleName.GetUnicode());
if (pExistingEntry != nullptr)
{
continue;
}
if (m_pTrustedPlatformAssemblyMap->LookupPtr(simpleName.GetUnicode()) != nullptr)
continue;

LPWSTR wszSimpleName = nullptr;
if (pExistingEntry == nullptr)
{
wszSimpleName = new WCHAR[simpleName.GetCount() + 1];
LPWSTR wszSimpleName = new WCHAR[simpleName.GetCount() + 1];
if (wszSimpleName == nullptr)
{
GO_WITH_HRESULT(E_OUTOFMEMORY);
}

wcscpy_s(wszSimpleName, simpleName.GetCount() + 1, simpleName.GetUnicode());

SimpleNameToFileNameMapEntry mapEntry{ wszSimpleName, nullptr };
m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry);
}
else
}
else
{
sTrustedPlatformAssemblies.Normalize();
for (SString::Iterator i = sTrustedPlatformAssemblies.Begin(); i != sTrustedPlatformAssemblies.End(); )
{
wszSimpleName = pExistingEntry->m_wszSimpleName;
}
SString fileName;
SString simpleName;
HRESULT pathResult = S_OK;
IF_FAIL_GO(pathResult = GetNextTPAPath(sTrustedPlatformAssemblies, i, /*dllOnly*/ false, fileName, simpleName));
if (pathResult == S_FALSE)
{
break;
}

LPWSTR wszFileName = new WCHAR[fileName.GetCount() + 1];
if (wszFileName == nullptr)
{
GO_WITH_HRESULT(E_OUTOFMEMORY);
}
wcscpy_s(wszFileName, fileName.GetCount() + 1, fileName.GetUnicode());
const SimpleNameToFileNameMapEntry *pExistingEntry = m_pTrustedPlatformAssemblyMap->LookupPtr(simpleName.GetUnicode());
if (pExistingEntry != nullptr)
{
continue;
}

LPWSTR wszSimpleName = new WCHAR[simpleName.GetCount() + 1];
if (wszSimpleName == nullptr)
{
GO_WITH_HRESULT(E_OUTOFMEMORY);
}
wcscpy_s(wszSimpleName, simpleName.GetCount() + 1, simpleName.GetUnicode());

SimpleNameToFileNameMapEntry mapEntry;
mapEntry.m_wszSimpleName = wszSimpleName;
mapEntry.m_wszILFileName = wszFileName;
LPWSTR wszFileName = new WCHAR[fileName.GetCount() + 1];
if (wszFileName == nullptr)
{
GO_WITH_HRESULT(E_OUTOFMEMORY);
}
wcscpy_s(wszFileName, fileName.GetCount() + 1, fileName.GetUnicode());

m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry);
SimpleNameToFileNameMapEntry mapEntry{ wszSimpleName, wszFileName };
m_pTrustedPlatformAssemblyMap->AddOrReplace(mapEntry);
}
}

//
Expand Down
17 changes: 13 additions & 4 deletions src/coreclr/binder/assemblybindercommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

#include "common.h"
#include "assemblybindercommon.hpp"
#include "hostinformation.h"
#include "assemblyname.hpp"
#include "assembly.hpp"
#include "applicationcontext.hpp"
Expand Down Expand Up @@ -897,11 +898,21 @@ namespace BINDER_SPACE
// Is assembly on TPA list?
SimpleNameToFileNameMap * tpaMap = pApplicationContext->GetTpaList();
const SimpleNameToFileNameMapEntry *pTpaEntry = tpaMap->LookupPtr(simpleName.GetUnicode());
SString fileName;
if (pTpaEntry != nullptr)
{
_ASSERTE(pTpaEntry->m_wszILFileName != nullptr);
SString fileName(pTpaEntry->m_wszILFileName);
if (pTpaEntry->m_wszILFileName != nullptr)
{
fileName.Set(pTpaEntry->m_wszILFileName);
}
else
{
HostInformation::ResolveAssemblyToPath(pTpaEntry->m_wszSimpleName, fileName);
}
}

if (!fileName.IsEmpty())
{
ReleaseHolder<Assembly> pAssembly;
SString getAssemblyDiag;
hr = GetAssembly(fileName,
Expand Down Expand Up @@ -1349,5 +1360,3 @@ BOOL AssemblyBinderCommon::IsValidArchitecture(PEKIND kArchitecture)

#endif // !defined(DACCESS_COMPILE)
};


7 changes: 0 additions & 7 deletions src/coreclr/binder/inc/applicationcontext.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,6 @@ namespace BINDER_SPACE

void OnDestructPerEntryCleanupAction(const SimpleNameToFileNameMapEntry & e)
{
if (e.m_wszILFileName == nullptr)
{
// Don't delete simple name here since it's a filename only entry and will be cleaned up
// by the SimpleName -> FileName entry which reuses the same filename pointer.
return;
}

if (e.m_wszSimpleName != nullptr)
{
delete [] e.m_wszSimpleName;
Expand Down
Loading