-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compiler intrinsics #1685
Compiler intrinsics #1685
Conversation
This is an alternate proposal for the compiler intrinsicts feature: dotnet#191 dotnet/roslyn#11475 This alternate design proposal comes after reviewing a prototype implementation of the original proposal by @mjsabby as well as the use throughout a significant code base. This design was done with significant input from @mjsabby, @tmat and @jkotas.
intrinsics.md
Outdated
Restrictions of this feature: | ||
|
||
- Properties cannot be used in a `handleof` expression. | ||
- The `handleof` expression cannot be used when there is an existing `handelof` name in scope. For |
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.
Typo: handelof
intrinsics.md
Outdated
[AttributeUsage(AttributeTargets.Method)] | ||
public sealed class CallIndirectAttribute : Attribute | ||
{ | ||
public CallingConvention CallingConvention { get; set; } |
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.
Should be read-only. Design guidelines say that attribute arguments should be either positional or named, not both.
This allows developers to define methods in the following form: | ||
|
||
``` csharp | ||
[CallIndirect(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 enum does not have a value for regular managed calling convention. (@mjsabby use case and some other use cases needs the managed calling convention).
I think we may need to have two attributes:
- CallIndirectAttribute for regular managed calls that does not have CallingConvention.
- UnmanagedCallIindirectAttribute for unmanaged calls that does have CallingConvention
An alternative is to add a value to for the managed calling convention to CallingConvention enum. I have rejected it because of the new value would be invalid in all existing APIs that use CallingConvention enum today. All these APIs are for unmanaged calls.
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.
add a value to for the managed calling convention to CallingConvention enum
Add a new value in CallingConvention seems better than use two attributes. simpler and easy to understand.
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.
@luqunl I agree in principle that two attributes are more annoying - however, I agree with @jkotas that adding it to the enum is less than ideal.
Another approach that would work from the API perspective would be to make the default constructor for CallIndirectAttribute
specify a sentinel value of sorts that indicates managed convention - a default that probably makes sense - and another constructor that takes a CallingConvention
enum value. In this case the default is 'managed to managed' and if a calling convention is used the intent is to leave the managed world so let us know what is being called.
The nice aspect about this is that the sentinel value can be anything and doesn't require breaking the API boundary.
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.
Let me add an open issues to track this down. The language design can proceed while we figure this out.
- 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. | ||
|
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 should productize the NativeCallableAttribute that will work together with this. This proposal together with NativeCallableAttribute will deliver complete solution for low-level managed/unmanaged interop for function pointers:
- address-of + NativeCallableAttribute can be used the get unmanager pointer of method implemented in C#
- Calli indirect can be used to call unmanaged pointer
.NET Core supports the NativeCallableAttribute attribute internally, but the feature was not exposed in public contracts and documented.
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.
What does this attribute do today in the internal support?
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 adds unmanaged->managed->unmanaged transition to the method prolog/epilog.
Marking the method with NativeCallableAttribute and taking its address is more efficient low-level equivalent of Marshal.GetFunctionPointerForDelegate. It avoids the delegate wrapping and unwrapping overhead. More details are in description for dotnet/coreclr#1566
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.
Marking the method with NativeCallableAttribute and taking its address is more efficient low-level equivalent of Marshal.GetFunctionPointerForDelegate
They are not exactly equivalent. The first one doesn't support marshalling(you need to use blittable types), the second one support marshalling.
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.
Chatted with @AaronRobinsonMSFT and I better understand how this could interact with the feature. Agree there are benefits. Seems easy to add if the runtime decides to productize this. Will add this to the "future consideration" section.
intrinsics.md
Outdated
|
||
### calli | ||
|
||
The compiler will add support for a new type of `extern` function that efficients translates into |
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.
Typo: efficients
intrinsics.md
Outdated
### calli | ||
|
||
The compiler will add support for a new type of `extern` function that efficients translates into | ||
a `.calli` instruction. The exten attribute will be marked with an attribute of the following |
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.
Typo: exten
intrinsics.md
Outdated
|
||
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, possible local function, that is unambiguous and uses C# code to call into the |
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 statement:
possible local function
contradicts statement above:
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.
intrinsics.md
Outdated
|
||
``` csharp | ||
unsafe { | ||
static void LocalLog() => Util.Log(); |
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.
Isn't this an example of a static local function discussed in Future Considerations below?
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. I was trying to keep the sample concise. I will make it clearer by using a plain old static.
intrinsics.md
Outdated
The addressof expression in this context will be implemented in the following manner: | ||
|
||
- ldftn: when the method is static | ||
- ldvirtftn: when the method is an instance method. |
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.
Would we allow the emission of ldftn if we know that the method is non-virtual?
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.
C# always uses callvirt
instead of call
and I was trying to mirror that here. I don't have any particular reason beyond that.
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.
Having a way to do both ldvirtftn
and ldftn
would let library writers determine whether a method has been overridden by a subclass. This has been used in performance optimization in the past, but it's unavailable to code that doesn't live in the 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.
BTW: There is nothing that says that the runtime has to return the same pointer from ldvirtftn
all the time. .NET Framework / Core do actually return different pointers for the same method.
If the pointer equality is used as an optimization, comparing function pointers will work but not reliably.
If the pointer equality is used for correctness, comparing function pointers is not going to work well. (Your HasOverriddenBeginEndRead example is used as both performance optimization but also to achieve correctness.)
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 is nothing that says that the runtime has to return the same pointer
We could add public API to compare function pointers for equality, couldn't we? Then this optimization would become available to library developers outside of CoreLib (provided we get a ldvirtftn
/ldftn
distinction in this proposal).
I'm calling this out because the original proposal in dotnet/roslyn#11475 had this, but this one doesn't. But I'll be happy if this makes it into Roslyn even in the currently proposed form.
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 difference between these two for non-virtual methods would only be about whether we get a NullReferenceException for a null this, right?
For call
and callvirt
this also affects binary compatibility. Imagine a method in a utility library changes from non-virtual to virtual and a consuming assembly isn't recompiled. In C# the code in the consuming assembly will automatically begin calling the method as a virtual.
I'm unsure if ldftn
and ldvirtftn
behave in a similar way. Been trying to track that down but haven't found any good dos on it. Assuming they do then it would be a consideration when deciding what code the address-of &
operator generated.
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
call
andcallvirt
this also affects binary compatibility
Isn't this just a side effect, as opposed from being a conscious design choice? When the compiler knows the this
is going to non-null, it will happily generate a call
:
public class Base
{
public void Frob()
{
}
}
class Derived : Base
{
public void Call()
{
// Emits call
Frob();
}
public static void CallStatic(Derived d)
{
// Emits callvirt
d.Frob();
// Emits call
d?.Frob();
}
}
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.
Isn't this just a side effect, as opposed from being a conscious design choice?
It's hard to say whether in C# 2.0 (where callvirt became the default over call) whether binary compat was a concern. At this point though it's a known quantity of the language that we consider when making changes. It's not an unbreakable change by any means but it's a consideration we make.
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 think we're over focusing on the callvirt thing here a bit. It's not a big blocking problem so much as it is a box that needs to be checked. I don't suspect there would be any substantial push back from LDM if we make a different decision for how ldftn
/ ldvirtftn
were chosen.
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.
Alright, LGTM.
intrinsics.md
Outdated
|
||
## Motivation | ||
|
||
The motivations and background for this feature are described in the following isssue (as is a |
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.
Typo isssue
How would the call indirect attribute mix with P/Invoke? |
DllImportAttribute and CallIndirectAttribute do not mix. Having both should produce compile error. |
intrinsics.md
Outdated
* [x] Proposed | ||
* [ ] Prototype: Not Started | ||
* [ ] Implementation: Not Started | ||
* [ ] Specification: Below |
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.
Will move this to the issue only.
The Static Delegates proposal would be fully emulatable in managed code if this was implemented. Perhaps it would be worth just implementing them simultaneously (assuming the additional overhead wasn't too great)? |
static delegates require further runtime work to be done in a safe fashion. There needs to be some way to prevent assembly unloading when a static delegate points inside it. This proposal side steps this problem by forcing all such operations to be |
@jaredpar, you could just make |
intrinsics.md
Outdated
### Disambiguating method groups | ||
|
||
There was some discussion around features that would make it easier to disambiguate method groups | ||
passed to an addressof expression. For instance potentially adding signature elements to the |
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.
addressof <- should be surrounded by back ticks (`)
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 not code though, it's a language concept.
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 think some people use it to highlight keywords as well 😄
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.
(whether language keywords or words that are key to the sentence)
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 a language concept, then it should be hyphenated "address-of" (i.e. 'addressof' is not a word).
intrinsics.md
Outdated
### handleof | ||
|
||
The `handleof` contextual keyword will translate a field, member or type into their equivalent | ||
`RuntimeHandle` using the `ldtoken` instruction. |
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.
Just for clarity - I was not familiar with RuntimeHandle
- it technically is RuntimeXXXXHandle
right? Want to make sure and not assume something new will be created. It might be beneficial to enumerate all of them at least once and then use the RuntimeHandle
as the placeholder going forward.
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.
Correct. I will clarify that here.
void* ptr = &Util.Log; | ||
``` | ||
|
||
Given there is no delegate conversion here the only mechanism for filtering members in the method |
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 we think that this is going to be a significant bar for existing libraries to use this feature, if they've already got a number of method overloads that they want to distinguish between?
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.
Ah, I see you have a section on that below, hadn't gotten there yet.
} | ||
``` | ||
|
||
This was rejected because a compelling case could not be made nor could a simple syntax be |
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.
Did we consider allowing a "cast"? ie, something like &((Action<string>)Util.Log)
?
|
||
``` csharp | ||
[CallIndirect(CallingConvention.Cdecl)] | ||
static extern int MapValue(string s, void *ptr); |
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 the C# compiler going to emit this method into the metadata, or will it disappear at source compilation time? The runtime typically won't load a type that has an extern
method without a DllImport
or some other special marking.
What happens if I try to reference this method in a non-call context (try to construct a delegate to this or get a RuntimeHandle of it)?
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 current implementation generates method bodies for this reason, and at least for the managed calling convention the calli gets inlined. Or would you like roslyn to prevent the cases you're mentioning and not emit metadata?
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 was just wondering if it's going to be actually emitted as extern
, or extern
was just a spare keyword.
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.
@MichalStrehovsky the extern
keyword here indicates the method will not have a body that is supplied by the developer. Instead it will be supplied by the compiler, or the runtime depending on how it's omitted. In this case as @mjsabby indicates we'll be putting the calli implementation in the body here.
This is still being fleshed out a bit and is more compiler centric vs. language centric. Hence I didn't dive too deep into that here.
The Project N compiler only needs the prefix to match, so matching the behavior here. dotnet/csharplang#1685 can't come soon enough. https://github.com/dotnet/corert/blob/7246a7a5f32f44dc12b8a538383a4bbc7cc26d91/src/System.Private.Interop/src/Shared/McgIntrinsics.cs#L59-L67
The Project N compiler only needs the prefix to match, so matching the behavior here. dotnet/csharplang#1685 can't come soon enough.
This allows developers to define methods in the following form: | ||
|
||
``` csharp | ||
[CallIndirect(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.
We should dis-allow DllImport method with CallIndirect attribute.
### Allow address of to target methods | ||
|
||
Method groups will now be allowed as arguments to an address-of expression. The type of such an | ||
expression will be `void*`. |
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.
curious: why do we prefer "void *" instead of "IntPtr"? If we use IntPtr, we don't need to use unsafe part?
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 not a safe operation which is part of the motivation for returning a void*
here.
|
||
``` csharp | ||
[CallIndirect(CallingConvention.Cdecl)] | ||
static extern int MapValue(string s, void *ptr); |
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 calli, Will we support generic method? also named arguments?
such as Int MapValue<T1, T2>(T1 a1, T2 a2, void *p)?
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.
Unsure about supporting a generic method. As long as it's supported in the runtime I think we can make it work fine in the language.
As for named arguments: yes. Likely optional as well.
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.
Generic calli is not supported by the 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 think @luqunl meant support for this:
.assembly foo { }
.method static !!0 Calli<TResult>(native int)
{
ldarg.0
calli !!0 ()
ret
}
.method static int32 Test()
{
ldc.i4 42
ret
}
.method static int32 Main()
{
.entrypoint
ldftn int32 Test()
call !!0 Calli<int32>(native int)
ret
}
Rather than generating a calli
whose signature is generic (something like calli !!0<!!0>()
).
This seems to work fine.
Allowing this on generic methods would save quite a bit of typing so I would vote for not blocking this.
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 agree that it would nice to allow it - if we do the matching work in CoreCLR and other runtimes to support it.
- For managed calling convention, it is fine and works.
- For unmanaged calling convention, it is going to hit https://github.com/dotnet/coreclr/issues/14524 in CoreCLR.
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.
Allowing this on generic methods would save quite a bit of typing so I would vote for not blocking this.
Yes. Interop would love to support this. Currently MCG and System.Private.Interop will define/generic a lot calli helper methods, similar to the following
internal static void HasThisCall__9<TArg0>(
object __this,
global::System.IntPtr pfn,
TArg0 arg0)
{
// This method is implemented elsewhere in the toolchain
}
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.
would save quite a bit of typing
I do not think that the extra typing is a problem here. This is a low-level feature where saving typing is not important. Also, this would be typically auto-generated, not typed by humans.
I would look at the potential support for generic unmanaged calli as second phase of this feature. Generic unmanaged calli would make this feature significantly more complex because of it would dependent on a runtime feature that does not exist yet.
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 would look at the potential support for generic unmanaged calli as second phase of this feature.
FWIW: expanding this to include generic support at a later date is basically a bug fix level change. I agree a second phase is a good landing place for this.
Generic unmanaged calli would make this feature significantly more complex because of it would dependent on a runtime feature that does not exist yet
It adds complexity yes but not significant complexity to the compiler. The compiler is already setup to look at RuntimeFeatures
to enable / disable items we know depend on runtime changes. As long as the fix in CoreClr included an entry here we could depend on it.
That being said I think we can move forward for now with generic disabled and revisit once we make a bit more progress. It won't really need language buy off at that point, just keyboard time to get the change in.
Updated PR based on feedback. |
This is an alternate proposal for the compiler intrinsicts feature:
#191
dotnet/roslyn#11475
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.