proposals/rejected/intrinsics.md
Champion issue: https://github.com/dotnet/csharplang/issues/191
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):
https://github.com/dotnet/csharplang/issues/191
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:
Restrictions of this feature:
&. 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:
RuntimeFieldHandleRuntimeTypeHandleRuntimeMethodHandleThe 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:
handleof expression.handleof expression cannot be used when there is an existing handleof 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:
DllImport attribute.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: