Skip to content

Conversation

teo-tsirpanis
Copy link
Contributor

This PR updates the bindings to the hostpolicy library to use function pointers for the callback parameters. This enables us to use UnmanagedCallersOnly for the callback functions in AssemblyDependencyResolver.

Because the callbacks in the hostpolicy APIs do not take an opaque void* parameter, we use a thread-local variable to store the callbacks' state. This is safe to do because:

  1. The callback passed to corehost_set_error_writer is documented to be per-thread.
  2. The callback passed to corehost_resolve_component_dependencies gets called only once at the end.

If you prefer to add new hostpolicy APIs, let me know.

@Copilot Copilot AI review requested due to automatic review settings August 23, 2025 17:38
@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 23, 2025
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Aug 23, 2025
@teo-tsirpanis teo-tsirpanis added area-Host and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Aug 23, 2025
Copilot

This comment was marked as outdated.

We now have function pointers and can write this with less magic.
@teo-tsirpanis teo-tsirpanis requested a review from Copilot August 23, 2025 20:36
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR modernizes the bindings to the hostpolicy library by replacing delegate-based callbacks with function pointers and UnmanagedCallersOnly methods. The change improves performance by eliminating delegate allocations and marshalling overhead while maintaining thread safety through thread-local storage for callback state.

Key changes:

  • Replaced delegates with function pointers in hostpolicy interop bindings
  • Implemented UnmanagedCallersOnly callback methods in AssemblyDependencyResolver
  • Added thread-local state management to safely store callback data without opaque parameters

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/libraries/Common/src/Interop/Interop.HostPolicy.cs Updated interop bindings to use function pointers instead of delegates
src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyDependencyResolver.cs Replaced delegate callbacks with UnmanagedCallersOnly methods and added thread-local state management
src/tests/Common/CoreCLRTestLibrary/HostPolicyMock.cs Updated mock implementation to use function pointers matching the new interop API

Comment on lines +42 to +43
ThreadLocalState state = new ThreadLocalState();
Debug.Assert(t_threadLocalState == null); // Re-entrant calls should not happen
Copy link
Preview

Copilot AI Aug 23, 2025

Choose a reason for hiding this comment

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

The Debug.Assert for re-entrant calls may not be sufficient protection in release builds. Consider throwing an exception or implementing proper re-entrancy handling, as the assertion is compiled out in release mode and concurrent access could corrupt the thread-local state.

Suggested change
ThreadLocalState state = new ThreadLocalState();
Debug.Assert(t_threadLocalState == null); // Re-entrant calls should not happen
if (t_threadLocalState != null)
throw new InvalidOperationException("Re-entrant calls to AssemblyDependencyResolver are not allowed.");

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not necessary; the callback will not run arbitrary code, but a specific function provided by the framework.

Comment on lines +203 to +204
ThreadLocalState? state = t_threadLocalState;
Debug.Assert(state != null);
Copy link
Preview

Copilot AI Aug 23, 2025

Choose a reason for hiding this comment

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

The Debug.Assert may not provide adequate protection in release builds. If t_threadLocalState is null, the callback will fail silently or cause a NullReferenceException on the next line. Consider using ArgumentNullException.ThrowIf or similar exception throwing mechanism.

Suggested change
ThreadLocalState? state = t_threadLocalState;
Debug.Assert(state != null);
ArgumentNullException.ThrowIfNull(state);

Copilot uses AI. Check for mistakes.

Comment on lines +213 to +214
ThreadLocalState? state = t_threadLocalState;
Debug.Assert(state != null);
Copy link
Preview

Copilot AI Aug 23, 2025

Choose a reason for hiding this comment

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

The Debug.Assert may not provide adequate protection in release builds. If t_threadLocalState is null, the callback will fail silently or cause a NullReferenceException on the next line. Consider using ArgumentNullException.ThrowIf or similar exception throwing mechanism.

Suggested change
ThreadLocalState? state = t_threadLocalState;
Debug.Assert(state != null);
if (state is null)
throw new InvalidOperationException("Thread local state is not set in ErrorWriterCallback.");

Copilot uses AI. Check for mistakes.

@vitek-karas
Copy link
Member

Curious what's the main motivation for this change? The description provides the reasoning of how... but not why.

@jkotas
Copy link
Member

jkotas commented Aug 27, 2025

Curious what's the main motivation for this change?

Our interop best practices say to prefer function pointers over marshalled delegates. This is one of the last few remaining instances of marshalled delegates in dotnet/runtime libraries. We got rid of them from as many places as possible.

  • Marshalled delegates have worse startup perf compared to UCO (marshalling IL stub requires runtime IL generation and JITing).
  • Marshalled delegates have worse throughput compared to UCO.
  • Marshalled delegates are bug prone. The code that uses them has to be very careful to keep the delegate alive as long as the marshalled pointer is used.
  • Marshalled delegates are not supported in locked down environments (Wasm, locked down Windows, SELinux). This point is not applicable here since this API is unlikely to be used in those environments.

@vitek-karas
Copy link
Member

The lack of context pointer is not ideal in the native API (sorry, my bad). If we could find a way to fix that, I would prefer that over the reliance on thread locals (although I agree it will work). But if it becomes too complicated I think it's OK to keep it as is.

@teo-tsirpanis
Copy link
Contributor Author

Opened #119139; I can do it in a subsequent PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Host community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants