This proposal provides language constructs that expose low level IL opcodes that cannot currently
be accessed efficiently, or at all: ldftn
, ldvirtftn
, ldtoken
and calli
. These low level
opcodes can be important in high performance code and developers need an efficient way to access
them.
The motivations and background for this feature are described in the following issue (as is a potential implementation of the feature):
This alternate design proposal comes after reviewing a prototype implementation of the original proposal by @msjabby as well as the use throughout a significant code base. This design was done with significant input from @mjsabby, @tmat and @jkotas.
Method groups will now be allowed as arguments to an address-of expression. The type of such an
expression will be void*
.
class Util {
public static void Log() { }
}
// ldftn Util.Log
void* ptr = &Util.Log;
Given there is no delegate conversion here the only mechanism for filtering members in the method group is by static / instance access. If that cannot distinguish the members then a compile time error will occur.
class Util {
public void Log() { }
public void Log(string p1) { }
public static void Log(int i) { };
}
unsafe {
// Error: Method group Log has more than one applicable candidate.
void* ptr1 = &Log;
// Okay: only one static member to consider here.
void* ptr2 = &Util.Log;
}
The addressof expression in this context will be implemented in the following manner:
- ldftn: when the method is non-virtual.
- ldvirtftn: when the method is virtual.
Restrictions of this feature:
- Instance methods can only be specified when using an invocation expression on a value
- Local functions cannot be used in
&
. The implementation details of these methods are deliberately not specified by the language. This includes whether they are static vs. instance or exactly what signature they are emitted with.
The handleof
contextual keyword will translate a field, member or type into their equivalent
RuntimeHandle
type using the ldtoken
instruction. The exact type of the expression will
depend on the kind of the name in handleof
:
- field:
RuntimeFieldHandle
- type:
RuntimeTypeHandle
- method:
RuntimeMethodHandle
The arguments to handleof
are identical to nameof
. It must be a simple name, qualified name,
member access, base access with a specified member, or this access with a specified member. The
argument expression identifies a code definition, but it is never evaluated.
The handleof
expression is evaluated at runtime and has a return type of RuntimeHandle
. This
can be executed in safe code as well as unsafe.
RuntimeHandle stringHandle = handleof(string);
Restrictions of this feature:
- Properties cannot be used in a
handleof
expression. - The
handleof
expression cannot be used when there is an existinghandleof
name in scope. For example a type, namespace, etc ...
The compiler will add support for a new type of extern
function that efficiently translates into
a .calli
instruction. The extern attribute will be marked with an attribute of the following
shape:
[AttributeUsage(AttributeTargets.Method)]
public sealed class CallIndirectAttribute : Attribute
{
public CallingConvention CallingConvention { get; }
public CallIndirectAttribute(CallingConvention callingConvention)
{
CallingConvention = callingConvention;
}
}
This allows developers to define methods in the following form:
[CallIndirect(CallingConvention.Cdecl)]
static extern int MapValue(string s, void *ptr);
unsafe {
var i = MapValue("42", &int.Parse);
Console.WriteLine(i);
}
Restrictions on the method which has the CallIndirect
attribute applied:
- Cannot have a
DllImport
attribute. - Cannot be generic.
The CallIndirectAttribute
as designed uses the CallingConvention
enum which lacks an entry for
managed calling conventions. The enum either needs to be extended to include this calling convention
or the attribute needs to take a different approach.
There was some discussion around features that would make it easier to disambiguate method groups passed to an address-of expression. For instance potentially adding signature elements to the syntax:
class Util {
public static void Log() { ... }
public static void Log(string) { ... }
}
unsafe {
// Error: ambiguous Log
void *ptr1 = &Util.Log;
// Use Util.Log();
void *ptr2 = &Util.Log();
}
This was rejected because a compelling case could not be made nor could a simple syntax be envisioned here. Also there is a fairly straight forward work around: simple define another method that is unambiguous and uses C# code to call into the desired function.
class Workaround {
public static void LocalLog() => Util.Log();
}
unsafe {
void* ptr = &Workaround.LocalLog;
}
This becomes even simpler if static
local functions enter the language. Then the work around
could be defined in the same function that used the ambiguous address-of operation:
unsafe {
static void LocalLog() => Util.Log();
void* ptr = &Workaround.LocalLog;
}
The original proposal allowed for metadata tokens to be loaded as int
values at compile time.
Essentially have tokenof
that has the same arguments as handleof
but is evaluated at
compile time to an int
constant.
This was rejected as it causes significant problem for IL rewrites (of which .NET has many). Such
rewriters often manipulate the metadata tables in a way that could invalidate these values. There
is no reasonable way for such rewriters to update these values when they are stored as simple
int
values.
The underlying idea of having an opaque handle for metadata entries will continue to be explored by the runtime team.
This refers to the proposal to allow the
static
modifier on local functions. Such a function would be guaranteed to be emitted as
static
and with the exact signature specified in source code. Such a function should be a valid
argument to &
as it contains none of the problems local functions have today.
The CLR has a feature that allows for managed methods to be emitted in such a way that they are
directly callable from native code. This is done by adding the NativeCallableAttribute
to
methods. Such a method is only callable from native code and hence must contain only blittable
types in the signature. Calling from managed code results in a runtime error.
This feature would pattern well with this proposal as it would allow:
- Passing a function defined in managed code to native code as a function pointer (via address-of) with no overhead in managed or native code.
- Runtime can introduce use site errors for such functions in managed code to prevent them from being invoked at compile time.