csharplang/proposals/function-pointers.md

246 lines
10 KiB
Markdown
Raw Normal View History

2018-09-14 06:31:30 +02:00
# Function Pointers
## Summary
This proposal provides language constructs that expose IL opcodes that cannot currently be accessed efficiently,
2018-10-15 17:25:48 +02:00
or at all, in C# today: `ldftn` and `calli`. These IL opcodes can be important in high performance code and developers
need an effecient way to access them.
2018-09-14 06:31:30 +02:00
## Motivation
2018-10-05 23:18:13 +02:00
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 is an alternate design propsoal to [compiler intrinsics](https://github.com/dotnet/csharplang/blob/master/proposals/intrinsics.md)
2018-09-14 06:31:30 +02:00
## Detailed Design
2018-10-11 02:46:36 +02:00
### funcptr
2018-10-15 17:25:48 +02:00
The language will allow for the declaration of function pointers using the `funcptr` contextual keyword. The
declaration and usage of function pointers closely resemble that of `delegate`:
2018-10-11 02:46:36 +02:00
``` csharp
2018-10-11 20:21:26 +02:00
unsafe class Example {
delegate void DAction(int a);
funcptr void FAction(int a);
void Example(DAction d, FAction f) {
d(42);
f(42);
}
}
2018-10-11 02:46:36 +02:00
```
These types are represented using the function pointer type as outlined in ECMA-335. This means invocation
of a `funcptr` will use `calli` where invocation of a `delegate` will use `callvirt` on the `Invoke` method.
2018-10-15 17:25:48 +02:00
Syntactically though invocation is identical for both constructs.
2018-10-11 02:46:36 +02:00
The `calli` instruction requires the calling convention be specified as a part of the invocation. The default
for `funcptr` will be managed. Alternate forms can be specified by adding the appropriate modifier after the
`funcptr` keyword: `cdecl`, `fastcall`, `stdcall`, `thiscall` or `winapi`. Example:
``` csharp
// This method will be invoked using the cdecl calling convention
2018-10-15 17:25:48 +02:00
funcptr cdecl int F1(int value);
2018-10-11 02:46:36 +02:00
2018-10-15 17:25:48 +02:00
// This method will be invoked using the stdcall calling convention
funcptr stdcall int F2(int value);
```
2018-10-11 20:21:26 +02:00
2018-10-15 17:25:48 +02:00
Conversions between `funcptr` types is done based on their signature, not name. When two `funcptr` declarations
have the same signature they have an identity conversion no matter what the name is.
2018-10-11 02:46:36 +02:00
``` csharp
2018-10-11 20:21:26 +02:00
unsafe class Example {
2018-10-15 17:25:48 +02:00
funcptr int Func1(int left, int right);
funcptr int Func2(int x);
funcptr int Func3(int x, int y);
funcptr cdecl int Func4(int x, int y);
2018-10-11 20:21:26 +02:00
void Conversions() {
2018-10-15 17:25:48 +02:00
Func1 p1 = ...;
Func2 p2 = ...;
2018-10-11 20:21:26 +02:00
2018-10-15 17:25:48 +02:00
Func3 p3 = p1; // okay Func1 and Func3 have compatible signatures
Console.WriteLine(p3 == p1); // True
Func3 p4 = p2; // error: Func3 and Func2 have incompatible signatures (parameter)
Func4 p5 = p1; // error: Func4 and Func1 have incompatible signatures (calling convention)
2018-10-11 20:21:26 +02:00
}
2018-10-11 02:46:36 +02:00
}
```
2018-10-11 20:21:26 +02:00
In addition to declaring a named `funcptr` type, as you declare a `delegate`, it is possible to use an unnamed
2018-10-15 17:25:48 +02:00
`funcptr` type without first declaring it. This type can be used anywhere a type declaration would occur:
2018-10-11 02:46:36 +02:00
2018-10-11 20:21:26 +02:00
``` csharp
unsafe struct Example {
funcptr int (int) Field;
unsafe void UnnamedExample(funcptr int(int) ptr) {
int x = ptr(42);
Field = ptr;
...
}
}
```
2018-09-14 06:31:30 +02:00
2018-10-15 17:25:48 +02:00
A `funcptr` type is a pointer type which means it has all of the capabilities and restrictions of a standard pointer
type:
- Only valid in an `unsafe` context.
- Methods which contain a `funcptr` parameter or return type can only be called from an `unsafe` context.
- Cannot be converted to `object`.
- Cannot be used an a generic argument.
- Can implicitly convert `funcptr` to `void*`.
- Can explicitly convert from `void*` to `funcptr`.
2018-09-14 06:31:30 +02:00
2018-10-15 17:25:48 +02:00
Restrictions:
2018-10-11 02:46:36 +02:00
- Cannot overload when the only difference in parameter types is the name of the function pointer.
- Custom attributes cannot be applied to a `funcptr` or any of its elements.
2018-10-11 20:21:26 +02:00
- A `funcptr` parameter cannot be marked as `params`
- A `funcptr` type has all of the restrictions of a normal pointer type.
2018-10-11 02:46:36 +02:00
2018-10-11 20:21:26 +02:00
### Allow addresss-of to target methods
2018-10-11 02:46:36 +02:00
2018-10-11 20:21:26 +02:00
Method groups will now be allowed as arguments to an address-of expression. The type of such an
2018-10-15 17:25:48 +02:00
expression will be an unnamed `funcptr` which has the equivalent signature of the target method and a managed
calling convention:
2018-10-11 02:46:36 +02:00
2018-10-11 20:21:26 +02:00
``` csharp
unsafe class Util {
public static void Log() { }
2018-09-14 06:31:30 +02:00
2018-10-11 20:21:26 +02:00
funcptr void Action();
funcptr int Func();
void Use() {
funcptr void() ptr1 = &Util.Log;
Action ptr2 = &Util.Log;
// Error: type "funcptr void()" not compatible with "funcptr int()";
Func ptr3 = &Util.Log;
// Okay. Conversion to void* is always allowed.
void* v = &Util.Log;
}
}
```
2018-09-14 06:31:30 +02:00
2018-10-11 20:21:26 +02:00
The conversion of an address-of method group to `funcptr` has roughly the same process as method group to `delegate`
2018-10-15 17:25:48 +02:00
conversion. There are two additional restrictions to the existing process:
- Only members of the method group that are marked as `static` will be considered.
- Only a `funcptr` with a managed calling convention can be the target of such a conversion.
This means developers can depend on overload resolution rules to work in conjunction with the
2018-10-11 20:21:26 +02:00
address-of operator:
2018-09-14 06:31:30 +02:00
2018-10-11 20:21:26 +02:00
``` csharp
unsafe class Util {
public static void Log() { }
public static void Log(string p1) { }
public static void Log(int i) { };
funcptr void Action1();
funcptr void Action2();
void Use() {
Action1 a1 = &Log; // Log()
Action2 a2 = &Log; // Log(int i)
// Error: ambiguous conversion from method group Log to "void*"
void* v = &Log;
}
```
The address-of operator will be implemented using the `ldftn` instruction.
Restrictions of this feature:
- Only applies to methods marked as `static`.
- 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.
### Better function member
The better function member specification will be changed to include the following line:
> A `funcptr` is more specific than `void*`
This means that it is possible to overload on `void*` and a `funcptr` and still sensibly use the address-of operator.
## Open Issuess
- Round tripping function pointer names, as well as parameter names, through metadata will require additional work. The
function pointer type itself is natively supported by CLI but that does not include any names. This is not anticipated
to be a big issue, just needs design work.
2018-10-15 17:25:48 +02:00
- The address-of operator is limited to `static` methods in this proposal. It can be made to work with instance methods
but the behavior can be confusing to developers. The `this` type becomes an explicit first parameter on the `funcptr`
type. This means the behavior and usage would differ significantly from `delegate`. This extra confusion was the main
reason it was not included in the design.
2018-09-14 06:31:30 +02:00
## Considerations
2018-10-11 20:21:26 +02:00
### Don't require unsafe at declaration
Instead of requiring `unsafe` at every use of a `funcptr`, only require it at the point where a method group is
converted to a `funcptr`. This is where the core safety issues come into play (knowing that the containing assembly
cannot be unloaded while the value is alive). Requiring `unsafe` on the other locations can be seen as excessive.
This is how the design was originally intended. But the resulting language rules felt very awkward. It's impossible to
hide the fact that this is a pointer value and it kept peeking through even without the `unsafe` keyword. For example
the conversion to `object` can't be allowed, it can't be a member of a `class`, etc ... The C# design is to require
`unsafe` for all pointer uses and hence this design follows that.
Developers will still be capable of preventing a _safe_ wrapper on top of `funcptr` values the same way that they do
for normal pointer types today. Consider:
``` csharp
unsafe struct Action {
funcptr void() _ptr;
Action(funcptr void() ptr) => _ptr = ptr;
public void Invoke() => _ptr();
}
```
2018-09-14 06:57:07 +02:00
### Using delegates
Instead of using a new syntax element, `funcptr`, simply use exisiting `delegate` types with a `*` following the type:
``` csharp
Func<object, object, bool>* ptr = &object.ReferenceEquals;
```
2018-10-11 02:46:36 +02:00
Handling calling convention can be done by annotating the `delegate` types with an attribute that specifies
a `CallingConvention` value. The lack of an attribute would signify the managed calling convention.
2018-09-14 06:57:07 +02:00
Encoding this in IL is problematic. The underlying value needs to be represented as a pointer yet it also must:
1. Have a unique type to allow for overloads with different function pointer types.
1. Be equivalent for OHI purposes across assembly boundaries.
2018-10-11 02:46:36 +02:00
The last point is particularly problematic. This mean that every assembly which uses `Func<int>*` must encode
an equivalent type in metadata even though `Func<int>*` is defined in an assembly though don't control.
Additionally any other type which is defined with the name `System.Func<T>` in an assembly that is not mscorlib
must be different than the version defined in mscorlib.
One option that was explored was emitting such a pointer as `mod_req(Func<int>) void*`. This doesn't
work though as a `mod_req` cannot bind to a `TypeSpec` and hence cannot target generic instantiations.
2018-09-14 06:57:07 +02:00
2018-10-11 20:21:26 +02:00
### No names altogether
Given that a `funcptr` can be used without names why even allow names at all? The underlying CLI primitive doesn't have
names hence the use of names is purely a C# invention. That ends up being a leaky abstraction in some cases (like
not allowing overloads when `funcptr` differ by only names).
At the same time, `funcptr` look and feel so much like `delegate` types, not allowing them to be named would be seen
as an enormous gap by customers. The leaky abstraction is wort the trade offs here.
### Requiring names always
Given that names are allowed for `funcptr` why not just require them always? Given that `funcptr` is an existing CLI
type there are uses of it in the ecosystem today. None of those uses will have the metadata serialization format
chosen by the C# compiler. This means the feature would be using CLI function pointers but not interopting with any
existing usage.
2018-09-14 06:31:30 +02:00
## Future Considerations
2018-10-11 20:21:26 +02:00
### static local functions
This refers to [the proposal](https://github.com/dotnet/csharplang/issues/1565) 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