-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add NativeLibrary class #16409
Add NativeLibrary class #16409
Conversation
@@ -864,6 +864,14 @@ public static void PrelinkAll(Type c) | |||
} | |||
} | |||
|
|||
internal static int NumParamBytes(RuntimeMethodInfo m, bool isForStdCallDelegate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is based somewhat on the full framework public API (https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal.numparambytes(v=vs.110).aspx).
|
||
// This is a *partial* copy of include/pal/module.h so that we can extract the dl_handle. | ||
[StructLayout(LayoutKind.Sequential)] | ||
private struct _MODSTRUCT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really a fan of this pattern, but this seemed safer than trying to have NativeLibrary.cpp reach deep into the PAL-specific header files. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could expose it as a PAL function, e.g. PAL_GetDlHandle(HMODULE hMod)
and pinvoke into it here (or expose a wrapper that calls this function as a QCALL) to get rid of this exposure of PAL internals.
It actually seems that moving the IsValidModuleHandle to the native part of the implementation and exposing it as a QCALL would be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the IsValidModuleHandle to the native part of the implementation
There is a lot of chattiness between the managed and native parts - there are 5 new FCalls/QCalls already. I think it would be better to have bulk of this implementation in C++, and not have this chattiness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree.. I would either all(or mostly) implements in native code or all implements in managed code. but not in middle. It will help interop team maintain these codes in long term..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we were developing this I chatted with quite a few people who have a vested interest in this type's implementation, and the recommendations eventually came down to: (a) reuse as much code as practical; and (b) if new code must be created keep as much of it managed as possible, even at the expense of performance. This would make the code easier to understand and would benefit maintenance in the long run.
In practice the only real implementation logic that exists in C# is a managed equivalent to the C++ FindEntryPoint
method. Tweaking and reusing the FindEntryPoint
method directly was dismissed due to how much refactoring it would need. All other C# code is parameter validation / error handling / etc., with p/invokes back into the runtime to do the real work.
If you feel strongly about moving the managed FindEntryPoint
logic back into C++ let me know and I'll defer to your judgment. Be aware that it will result in larger, more complex, less readable code, with minimal visible effects for consumers of this type. It also risks slipping this type from 2.1 to vNext.
pAssembly = callingAssembly->GetAssembly(); | ||
} | ||
|
||
hmod = NDirect::LoadLibraryModuleForNativeLibrary(moduleName, pAssembly, searchAssemblyDirectory, searchPaths); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NDirect
has traditionally only been used for [DllImport]
, not for Marshal.GetDelegateForFunctionPointer
. This still seems like an ok dependency though because we're basically trying to mimic [DllImport]
logic. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is a OK dependency. I would prefer move into dllimport.cpp, so it will remove this dependency
|
||
namespace System.Runtime.InteropServices | ||
{ | ||
// Contains UNIX-specific logic for NativeLibrary class. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, "UNIX" also includes OSX.
/// <summary> | ||
/// Equivalent to <see cref="TryGetDelegate{TDelegate}(string, bool, out TDelegate)"/> where <em>exactSpelling</em> is <see langword="false"/>. | ||
/// </summary> | ||
public bool TryGetDelegate<TDelegate>(string name, out TDelegate result) where TDelegate : class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we'd use where TDelegate : delegate
, but that language feature doesn't exist :(. This is the next best constraint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where TDelegate : System.Delegate
is coming in C# 7.3 (as is : System.Enum
, you just can't use the keywords, delegate
and enum
, as of the current iteration)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the PR to support the constraints looks about as good as done (and consuming the delegate/enum constraints has always been supported anyway).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know when that version of the compiler will be made available to the coreclr and corefx projects? If it's dropping imminently then we can probably get the change in time for 2.1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe @VSadov would know. It requires, at the very least, for the features/constraints
branch to be merged back into master
before it is available.
retVal = CreateDelegateForSymbolByName<TDelegate>(name + "W") ?? retVal; | ||
} | ||
|
||
#if (BIT32 && !ARM) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this is X86 specific, it should be under #if X86
and not this.
charSet = default; | ||
callingConvention = default; | ||
|
||
var attr = delegateType.GetCustomAttribute<UnmanagedFunctionPointerAttribute>(inherit: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think we should be duplicating the UnmanagedFunctionPointerAttribute parsing logic here. We should just use the one we have in unmanaged runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Same for other pieces of logic that are duplicated in the unmanaged runtime.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can remove ParseCharsetAndCallingConvention
by reusing existing PInvokeStaticSigInfo
logic in dllimport.h. However, I don't think I can reuse existing unmanaged FindEntryPoint
logic without introducing a significant refactoring, and others I spoke with offline offered that the current proposal of rewriting the logic in managed code would be much less risky.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I does not have to reusing the code if it is unnatural. But I think having a method with same structure as NDirectMethodDesc::FindEntryPoint
right next to NDirectMethodDesc::FindEntryPoint
to handle this case would be better.
// the value immediately following the '#' can fit into a WORD), then process the | ||
// ordinal and skip all other logic in this routine. | ||
|
||
if (AllowLocatingFunctionsByOrdinal && name.Length >= 1 && name[0] == '#') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this functionality we need to support? It's unfortunate that a new API whose purpose seems targeted at being cross-platform would have platform-specific behavioral differences designed in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exporting by ordinal is still actively recommended to customers as a way to reduce DLL size on disk. See https://docs.microsoft.com/en-us/cpp/build/determining-which-exporting-method-to-use and https://docs.microsoft.com/en-us/cpp/build/exporting-functions-from-a-dll-by-ordinal-rather-than-by-name.
// TODO: What should the exception message say? | ||
throw new Exception("Must specify DllImportSearchPaths if assembly not specified."); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to the low byte being reserved, I think it's important to check that the enum value being passed in is one we understand. Unmapped values will lead to differing behaviors on Windows and non-Windows systems, and we should endeavor to prevent that from happening in new API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how we could do that without interfering with the scenario. The only values understood by non-Windows platforms are AssemblyDirectory
and LegacyBehavior
. We can't eagerly fail on other values being passed in because it would block the scenario below.
NativeLibrary lib;
if (NativeLibrary.TryLoad("foo.dll", null, System32, out lib))
{
// We're on Windows, use OS-provided foo.dll.
}
else if (NativeLibrary.TryLoad("foo.so", typeof(me).Assembly, AssemblyDirectory, out lib))
{
// We're on non-Windows, use our local equivalent.
}
We don't want the call to TryLoad("foo.dll", ...)
to throw since it's really nothing more than a glorified OS check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We spoke offline about this. We can throw as long as the caller has the ability to conditionally guard the call. For example, the guarded call site could look like this.
NativeLibrary lib;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (!NativeLibrary.TryLoad("foo.dll", ..., System32, out lib))
{
throw new Exception("Expected this DLL to be found.");
}
}
else
{
if (!NativeLibrary.TryLoad("bar.so", ..., Assembly, out lib))
{
throw new Exception("Expected this DLL to be found.");
}
}
This pattern would also be more resilient to DLL poisoning attacks because the OS version check is performed up front, allowing the caller to pass the search paths appropriate for the OS.
- Forbid invalid flags on TryLoad - Define 'X86' constant - Use existing PInvokeStaticSigInfo logic to crack open the attribute - Fix compilation error on non-Windows
|
||
// Per PInvokeStaticSigInfo::InitCallConv and GetDefaultCallConv, the default calling convention | ||
// on Unix is cdecl. | ||
private const CallingConvention FallbackCallingConvention = CallingConvention.Cdecl; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may require msdn doc update, since in MSDN(https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.callingconvention(v=vs.110).aspx),
It states that "The callee cleans the stack. This is the default convention for calling unmanaged functions with platform invoke."
RuntimeMethodHandleInternal invokeMethodHandle = new RuntimeMethodHandleInternal(StubHelpers.StubHelpers.GetDelegateInvokeMethodFromDelegateType(delegateType)); | ||
RuntimeMethodInfo invokeMethodInfo = (RuntimeMethodInfo)RuntimeType.GetMethodBase(delegateType, invokeMethodHandle); | ||
|
||
// Generic types are disallowed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why generic delegates is n't allowed? Func or Action should be okay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that the check here was supposed to be for delegateType.ContainsGenericParameters
, aka delegateType.IsOpenType
(that property doesn't actually exist).
Using ContainsGenericParameters
allows typeof(Func<bool>)
, but not typeof(Func<>)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an existing limitation of Marshal.GetDelegateForFunctionPointer. Many of these checks are duplicates of the checks performed by that method; I'm just performing the checks upfront so that we don't go too far down the "find the entry point" logic if we know we're going to fail immediately after.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh. Weird. Well, that's sufficient for me. (Though now I wonder why GetDelegateForFunctionPointer has that restriction. But, it already exists, so OK.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I had to guess (and this is only a guess), I'd venture that the internal marshaling system has trouble specializing generics for reference types. For example, consider Func<string, int>
and Func<SafeHandle, int>
. They're both represented under the covers as a single Func<__Canon, int>
type, but string
and SafeHandle
have very different requirements for marshaling. This would seemingly greatly complicate the marshaling stub lookup. I suppose it would be possible to support this but due to lack of interest nobody ever took the time to allow it.
// LoadLibrary | ||
// Loads the specified library, returning its handles, or nullptr if library cannot be found | ||
HINSTANCE QCALLTYPE NativeLibrary::LoadLibrary(LPCUTF8 moduleName, QCall::AssemblyHandle callingAssembly, BOOL searchAssemblyDirectory, DWORD searchPaths) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it possible that expose these functions(overload is fine) into dllimport.cpp, so that all Pinvoke related functions are implemented in dllimport.cpp?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these functions are implemented in dllimport.cpp adjacent to the existing code. This is just the QCALL entry point with parameter validation, similar to how comdelegate.cpp and related files already serve as the QCALL entry points into existing dllimport.cpp code.
{ | ||
// The allowed mask values for the DllImportSearchPath. | ||
// Non-Windows sytems only allow AssemblyDirectory and LegacyBehavior. | ||
private const uint AllowedDllImportSearchPathsMask = (uint)DllImportSearchPath.AssemblyDirectory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth a comment that LegacyBehavior is 0, so it's implicitly in this list. (So it doesn't look like a mismatch to the current comment)
/// <remarks> | ||
/// Functions can also be queried by ordinal export by using a "#" prefix (e.g., "#1") for <paramref name="name"/>. | ||
/// </remarks> | ||
public bool TryGetDelegate<TDelegate>(string name, bool exactSpelling, out TDelegate result) where TDelegate : class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a little concern for me. since it is very easy for customer to forgot to add CallConvention attribute for this Delegate Type. Maybe we should add a MSDN page to show how to use this API correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Marshal.GetDelegateForFunctionPointer
API has a fallback behavior if the [UnmanagedFunctionPointer]
attribute isn't present. This API tries to honor that same logic to the best of its ability. I agree that documentation will be necessary for correct usage of this API.
/// <remarks> | ||
/// Functions can also be queried by ordinal export by using a "#" prefix (e.g., "#1") for <paramref name="name"/>. | ||
/// </remarks> | ||
public bool TryGetDelegate<TDelegate>(string name, bool exactSpelling, out TDelegate result) where TDelegate : class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you try to add support for SetLastError? also others fields in DllImportAttribute
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The existing marshaling code handles that automatically. We get it for free.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For reference, this comes from the existing Marshal.GetDelegateForFunctionPointer
behavior. It respects any [UnmanagedFunctionPointer]
attribute specified on the delegate type, including the SetLastError field if specified. Since we're really just a glorified wrapper around Marshal.GetDelegateForFunctionPointer
we get it for free.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This need to add a doc(or sample code) to show customer how to do it correctly.
Removing "WIP" marker as the implementation is now code complete. Unit tests and reference APIs will be added in dotnet/corefx#27258. Have performed manual verification on Windows and Ubuntu independent of the unit tests. |
@@ -54,7 +54,7 @@ | |||
// | |||
// NumParamBytes | |||
// Counts # of parameter bytes | |||
INT32 QCALLTYPE MarshalNative::NumParamBytes(MethodDesc * pMD) | |||
INT32 QCALLTYPE MarshalNative::NumParamBytes(MethodDesc * pMD, CLR_BOOL fForStdCallDelegate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CLR_BOOL
is not correct here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, will fix.
I do see QCALLTYPE
and CLR_BOOL
used together in a few places, such as
coreclr/src/vm/comthreadpool.h
Line 38 in fae8cc7
static void QCALLTYPE InitializeVMTp(CLR_BOOL* pEnableWorkerTracking); |
|
||
if (paths == DllImportSearchPath.LegacyBehavior) | ||
{ | ||
var attr = caller.GetCustomAttribute<DefaultDllImportSearchPathsAttribute>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unmanaged runtime has cache for this value to avoid reflection call every time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going for simplicity here rather than performance. The action of having the OS load a module into the current process is by far going to dominate the run time of this method.
Will be reintroduced later when the implementation is cleaner
Why? |
I'm not convinced I can perform the requested cleanup to move the Handle validation / unwrapping logic into native code under the deadline. This allows us to add it in vNext using the preferred pattern. |
Punted to Future. Will send new PR at later date. |
This is the prototype for the
NativeLibrary
type based on the latest approved API design. Addresses https://github.com/dotnet/corefx/issues/17135.The behavior of each public API should be clearly described via the devdoc, but please let me know if something's unclear and we can change it or clarify the comments.
/cc @ianhays @bartonjs @morganbr