Skip to content
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

Introduce mechanism to indicate arguments are to be marshalled as native varargs #48796

Open
AaronRobinsonMSFT opened this issue Feb 26, 2021 · 65 comments
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.Runtime.InteropServices
Milestone

Comments

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Feb 26, 2021

Background and Motivation

Native varargs are a complicated interop scenario to support. At present, native varargs are only supported on the Windows platform through the undocumented __arglist keyword. Supporting varargs naturally in a P/Invoke scenario would be difficult from the C# language. However, it is possible to compromise by permitting support for the call with a fully specified DllImport signature and a hint from the user.

User scenario: #48752

Proposed API

namespace System.Runtime.InteropServices
{
+    [AttributeUsage(AttributeTargets.Method)]
+    public class NativeVarargsAttribute : Attribute
+    {
+        public NativeVarargsAttribute() { VarargBeginIndex = 0; }
+
+        /// <summary>
+        /// Zero-based index of the first variable argument.
+        /// </summary>
+        public int VarargBeginIndex;
+    }
}

Usage Examples

Consider the following native export with varargs:

void Varargs(int n, ...);

The following P/Invoke declarations would enable users to call and properly forward the arguments in a supported multi-platform manner.

[NativeVarargsAttribute(VarargBeginIndex = 1)]
[DllImport(@"NativeLibrary.dll", EntryPoint = "Varargs")]
static extern void Varargs0(int n);

[NativeVarargsAttribute(VarargBeginIndex = 1)]
[DllImport(@"NativeLibrary.dll", EntryPoint = "Varargs")]
static extern void Varargs1(int n, int a);

[NativeVarargsAttribute(VarargBeginIndex = 1)]
[DllImport(@"NativeLibrary.dll", EntryPoint = "Varargs")]
static extern void Varargs2(int n, int a, int b);

Alternative designs

Encode the information in the CallingConvention enum. This approach does remove the overhead of attribute reading, but does miss the added data of knowing where the varargs start - at present doesn't appear to be needed. This approach also impacts existing metadata tooling (for example, ILDasm, ILAsm, and ILSpy). See #48796 (comment).

public enum CallingConvention
{
+    VarArg = 6
}

Current state

Without this feature, calling functions with native varargs isn't possible on a non-Windows platforms. The proposed workaround is to create native shim libraries and instead P/Invoke into them. Continuing the example above, the shim library would export the following:

extern void Varargs(int n, ...);

void Varargs0(int n)
{
    Varargs(n);
}
void Varargs1(int n, int a)
{
    Varargs(n, a);
}
void Varargs2(int n, int a, int b)
{
    Varargs(n, a, b);
}

References

Support on Windows: dotnet/coreclr#18373
JIT details: #10478

@AaronRobinsonMSFT AaronRobinsonMSFT added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.InteropServices labels Feb 26, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Feb 26, 2021
@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Feb 26, 2021

@AaronRobinsonMSFT AaronRobinsonMSFT removed the untriaged New issue has not been triaged by the area owner label Feb 26, 2021
@AaronRobinsonMSFT AaronRobinsonMSFT added this to the Future milestone Feb 26, 2021
@sdmaclea
Copy link
Contributor

/cc @jkotas @janvorli

@jkoritzinsky
Copy link
Member

The _arglist keyword is supported for interop with P/Invoke on Windows platforms and the JIT already knows how to understand the encoding.

I think we should just use the _arglist keyword instead of adding a new attribute. The attribute read is much more expensive.

@sdmaclea
Copy link
Contributor

Maybe the trick is undocumented -> documented

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Feb 26, 2021

I think we should just use the _arglist keyword instead of adding a new attribute. The attribute read is much more expensive.

Not sure the attribute read should be a concern here to be honest. The generation of the stub is likely to be more costly than that. Also that read would be a single time.

Also, this is not about supporting varargs as it looks on Windows. This is about supporting the calling of varargs with a fully typed P/Invoke. The keyword would have no where to go and is not a placeholder we are likely to want.

@tannergooding
Copy link
Member

Supporting varargs naturally in a P/Invoke scenario would be difficult from the C# language.

What is the difficulty here? Is it not largely just the same as it does today, which is to push the args onto the execution stack and so (other than it being not an official keyword) it would largely just be the runtime handling it correctly for other platforms?

@jkotas
Copy link
Member

jkotas commented Feb 26, 2021

the attribute read

This can be fixed by combining this with adding a new member to CallingConvention enum. We would pay the attention to the advanced calling convention attributes only for the Extended calling conventions.

public enum CallingConvention
{
    Extended = 6
}

Alternatively, this can be just a new CallingConvention value:

public enum CallingConvention
{
    VarArg = 6
}

__arglist keyword

The __arglist keyword is poorly supported corner case. See e.g. dotnet/docs#18714 .

We should also think how we would enable this with function pointers. __arglist keyword is not supported with function pointers today.

varargs as it looks on Windows

Note that varargs interop on Windows supports both forward and reverse interop, and both ... and va_list. It is what makes it very complex and expensive to just do what we do on Windows accross all platforms.

@sdmaclea
Copy link
Contributor

sdmaclea commented Feb 26, 2021

What is the difficulty here?

ABIs vary across platforms. Some platforms can treat vararg arguments differently than normal arguments. On Apple Silicon, they are definitely treated differently.

So for interop we need to be explicit to support al platforms correctly.

@AaronRobinsonMSFT
Copy link
Member Author

This can be fixed by combining this with adding a new member to CallingConvention enum.

I was avoiding that since it would be encoded directly in metadata through DllImport. My previous experience was we prefer to avoid that given the impact to tooling.

@tannergooding
Copy link
Member

ABIs vary across platforms. Some platforms can treat vararg arguments differently than normal arguments.

Sure, but I don't see why __arglist (or a new properly supported keyword) cannot be properly handled by the JIT to be the correct ... for a given platform. The JIT is entirely stack based and it is free to translate that as appropriate to be registers, stack, or whatever else is appropriate for the ABI. It has to do this for all arguments already.

A method in native is written as void method(...) and is then handled via va_start, va_arg, va_copy, va_end, and va_list. So one would expect that we could simply have a corresponding concept for ... in .NET (currently __arglist which translates to the ECMA 335 vararg convention) and the relevant helpers (currently ArgIterator).

So I'm not seeing what is blocking the same from working on say Unix or Apple Silicon, other than the JIT not correctly handling these on non-windows. I would expect that if we just simply implemented the ABI defined (https://gitlab.com/x86-psABIs/x86-64-ABI for System V) then both forward and reverse P/Invoke would work (and that this is necessary anyways for whatever new concept is exposed)

@AaronRobinsonMSFT
Copy link
Member Author

Sure, but I don't see why __arglist (or a new properly supported keyword) cannot be properly handled by the JIT to be the correct ... for a given platform.

I don't think there is any reason other than this proposal is for a non-language impacting update :-) We can also start a conversation with Roslyn about how one would express this in C#.

@jkotas
Copy link
Member

jkotas commented Feb 26, 2021

I was avoiding that since it would be encoded directly in metadata through DllImport. My previous experience was we prefer to avoid that given the impact to tooling.

Yes, it has an impact on tooling. My actual concern is that we need a scalable pattern to add new calling conventions for DllImport. Let's say that we add 10 new calling conventions. What is the pattern we want to follow? Is it going to be 10 different attributes?

@tannergooding
Copy link
Member

tannergooding commented Feb 26, 2021

I don't think there is any reason other than this proposal is for a non-language impacting update

__arglist is a "reserved" keyword today, you can't even name a class with it (at least in Roslyn you get error CS0190: The __arglist construct is valid only within a variable argument method if you try). Maybe its as simple as just spec'ing the thing that has always been there and making it an "official" keyword? It's not pretty, but it's also really only for P/Invoke scenarios, so maybe that isn't the worst thing.

@AaronRobinsonMSFT
Copy link
Member Author

Let's say that we add 10 new calling conventions. What is the pattern we want to follow? Is it going to be 10 different attributes?

Great question. I've not spent much time considering that as it relates to DllImport. One thing I think we can all agree on is the desire to not have 10 attributes.

Maybe its as simple as just spec'ing the thing that has always been there and making it an "official" keyword? It's not pretty, but it's also really only for P/Invoke scenarios, so maybe that isn't the worst thing.

Sure, seems like a reasonable perspective. The __ prefix does have some implementation/undocumented semantics associated with it so it would need to be a new keyword for proper support and thus language impacting.

@jkotas
Copy link
Member

jkotas commented Feb 26, 2021

If we go with some variant of __arglist keyword, we should also figure out how it is going to work with the interop source generators that is our forward looking interop story.

@tannergooding
Copy link
Member

tannergooding commented Feb 26, 2021

Sure, seems like a reasonable perspective. The __ prefix does have some implementation/undocumented semantics associated with it so it would need to be a new keyword for proper support and thus language impacting.

I think that's fine. It might be that they say __arglist just becomes official and it could be that they say they now support ... or arglist contextually or something else. But I'd also expect (and hope) its "less" work given the existance of __arglist already wired fairly end to end (but someone from LDM could confirm).

If we go with some variant of __arglist keyword, we should also figure out how it is going to work with the interop source generators that is our forward looking interop story.

What consideration is needed here? Is this exposing a managed helper signature because exposing a managed method which takes ... is undesirable?

I had already experimented with having ClangSharp recognize variadic functions and generate __arglist but never checked it in because it was Windows only and not officially supported. The pattern seems fairly straightforward...

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Feb 26, 2021

What consideration is needed here? Is this exposing a managed helper signature because exposing a managed method which takes ... is undesirable?

Yes basically. Consider how our prototype source gen works:

[GeneratedDllImport(NativeExportsNE_Binary, EntryPoint = "sumi")]
public static partial int Sum(int a, <... or __arglist>);

The above varargs would need to be marshalled in some manner. Which means the variable argument list would need to be inspected in an efficient way at run time to perform that marshalling and forward those arguments.

@tannergooding
Copy link
Member

Could the source generator not just do the concrete overloads. Say for example you had

[GeneratedDllImport("msvcrt", EntryPoint = "printf")]
public static int Print(string format, __arglist);

For this, the generator would create:

[DllImport("msvcrt")]
private static extern printf(sbyte* format, __arglist);

And for each unique invocation it found, it would generate a helper override that is a more exact match.
For example, if you did Print("%s%s%s", "Hello", ", ", "World!"); and Print("%g", 100.0f);

The following two helpers would be generated:

public static partial int Print(string format, string arg0, string arg1, string arg2)
{
    fixed (byte* pFormat = Encoding.GetUTF8Bytes(format))
    fixed (byte* pArg0 = Encoding.GetUTF8Bytes(arg0))
    fixed (byte* pArg1 = Encoding.GetUTF8Bytes(arg0))
    fixed (byte* pArg2 = Encoding.GetUTF8Bytes(arg0))
    {
        return printf(pFormat, pArg0, pArg1, pArg2);
    }
}

public static partial int Print(string format, float arg0)
{
    fixed (byte* pFormat = Encoding.GetUTF8Bytes(format))
    {
        return printf(pFormat, format);
    }
}

The only "loose" end would be that nothing was generated for the original public static int Print(string format, __arglist) and a decision on what to do here would need to be handled.

@AaronRobinsonMSFT
Copy link
Member Author

And for each unique invocation it found, it would generate a helper override that is a more exact match.

Yep. Until a library decided to expose the P/Invoke directly at which point it may not observe any concrete calls.

@tannergooding
Copy link
Member

Yep. Until a library decided to expose the P/Invoke directly at which point it may not observe any concrete calls.

Isn't that the same world we already have today if someone does the following on Windows?

[DllImport("msvcrt")]
public static extern printf(sbyte* format, __arglist)

Can we not maintain the same support or can we not just block non-blittable parameters here (like we do for generics and a few other scenarios)?

@AaronRobinsonMSFT
Copy link
Member Author

Isn't that the same world we already have today if someone does the following on Windows?

I don't think so. I believe a library can export that signature as is today without issue. But your proposal for the source generator approach wouldn't work for that case because it won't see the callsite when the application calls the libraries export.

Can we not maintain the same support or can we not just block non-blittable parameters here (like we do for generics and a few other scenarios)?

I guess we could impose that, but my perspective is it isn't worth it. The proposed approach makes calling a vararg function possible and will naturally work with source generators in all scenarios since we tell the JIT how to pass the arguments. Supporting the gamut of varargs doesn't seem to be worth the cost at this point. It would impose a large burden on the source generator because the convention would need to be forwarded properly and the language updated. I'm simply not seeing the value in the cost to make it fully supported.

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Feb 26, 2021

I'm simply not seeing the value in the cost to make it fully supported.

Actually, I think it was also pointed out in #48796 (comment) that a lot of the JIT work will be the same - I agree with that. We can view this proposal with its requirement of a precise signature to be a down payment on enabling full support. Since if this proposal was accepted all we would need to do is address two additional issues:

  1. Make it an official scenario in C#.
  2. Ensure we have the facilities to make it work with source generators.

I think this proposal simply starts the journey towards full support in an MVP manner.

@tannergooding
Copy link
Member

tannergooding commented Feb 26, 2021

So you're basically thinking something like the following for the minimum viable product and then "full" varargs with language support might come later and would also enable the same on function pointers?

[NativeVarargsAttribute(VarargBeginIndex = 1)]
[DllImport(@"NativeLibrary.dll", EntryPoint = "Varargs")]
static extern void Varargs1(int n, int a);

That doesn't sound terrible, but it seems to me like you still have the problem that the user needs to know all overloads they require up front and that in the future you have two technologies to choose from.
And at first thought, it seems like something that could equally be handled by [GeneratedVarargs(new Type[] { typeof(arg0), typeof(arg1) })] on the method that has GeneratedDllImport for the __arglist case.

The only unique issues to ... (at least that I'm seeing) is that:

  • if a user publicly exports it (or calls a manual pinvoke without a source generator), what do you do when they pass in string or other non-blittable parameters
  • can the language commit to supporting __arglist, ..., or some other new keyword syntax within the required timeframe

It would seem like the MVP would be to just block the first scenario and ask C# on the second (and fallback to something different if C# can't commit).
Requiring users to pass blittable parameters for ... and to use one of the source generator attributes otherwise seems reasonable for an MVP. If that scenario is very important, it could be unblocked in the future.
Then it is functionally no different than the original proposal, including in generating the fixed signatures up front based on the overloads the user indicates they desire, just using the same thing that eventually becomes "full support".
However, it does enable automatic generation of those fixed signatures in the case the export is not public and also enables power users to explicitly pass in blittable parameters if that is preferred instead.

@sdmaclea
Copy link
Contributor

@AaronRobinsonMSFT Instead of adding NativeVarargsAttribute could we add a new optional parameter to DllImport which did the same thing. Might have a default not varargs value by default.

ta264 added a commit to Servarr/SQLite.Build that referenced this issue Nov 26, 2021
ta264 added a commit to Servarr/SQLite.Build that referenced this issue Nov 26, 2021
ta264 added a commit to Servarr/SQLite.Build that referenced this issue Nov 27, 2021
* Work around macOS arm64 varargs issues

See dotnet/runtime#48752
dotnet/runtime#48796

* Better patch from Taloth
@frindler
Copy link

frindler commented Apr 3, 2022

Is there any progress on supporting varargs in P/Invoke'd native libs? I just patched the padding for varargs to the 9th argument position into Libgit2sharp on OS X arm64, but there should be way to do this without this hack... (which may break again in the future)

@jgcodes2020
Copy link

Just my two cents:

C#, F#, and VB.Net all have a feature where variadic arguments can be passed via a specially marked array parameter at the end. Coupled with an attribute to identify it, perhaps you could have:

[DllImport("libc.so", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern void printf(string format, 
  [MarshalAs(UnmanagedType.VariableArguments)] params object[] args)

@webczat
Copy link
Contributor

webczat commented Aug 20, 2022 via email

@TomEdwardsEnscape
Copy link

TomEdwardsEnscape commented Jun 17, 2023

When I read through the history of this issue, I can see a clear case of perfect becoming the enemy of good. Attempts to support declaring variadic parameters in metadata have soaked up time and energy that could have been spent supporting variadic methods on Mac and Linux at all.

I came across this problem when wrapping Apple's objc_msgSend, which is one of those "it does everything" methods through which a huge number of unrelated calls are routed. (It's usually unwise to write such a method, but I'll give Apple a pass in this case since it's part of the Objective-C runtime and there is supposed to be an entire compiled language sat on top of it.)

Even if we could write a delegate and specify its variadic parameters, we don't want to because there are so many different ways to call objc_msgSend. Many are unique, at least within the set of methods/properties that we want to use.

__arglist is already the perfect solution for our needs. We don't care that it's slower than theoretical alternatives and not type safe. We just want to wrap this one specific call, pass it the appropriate arguments, then move on.

Here is a trimmed version of the code that I would like to write:

public class NSRunningApplication : NSObject
{
    // 1. Define a variadic native method
    [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
    private static extern nint objc_msgSend(nint receiver, nint selector, __arglist);

    // 2. Pass an __arglist
    public static NSRunningApplication? RunningApplicationWithProcessIdentifier(int pid) => 
        FromPointer(objc_msgSend(Class, RunningApplicationWithProcessIdentifierSelector, __arglist(pid)));
}

As we well know, this fails at runtime because __arglist is only supported on Windows.

Since we only need a limited number of Objective-C interop calls, we will proceed with the hack of padding the parameter list on Arm64. This is a very poor solution, but at least it avoids having to compile a new native library full of very specific objc_msgSend exports for my team's otherwise 100% managed codebase.

@AaronRobinsonMSFT
Copy link
Member Author

__arglist is already the perfect solution for our needs.

It doesn't though. The __arglist concept is for native variadic arguments - let's ignore the platform specific nature of it for now. The objc_msgSend suite of APIs aren't native variadic arguments. The original signature did use ..., which confused many people, but the use of the API was not using "varargs" in most cases, but rather the signature of the target function. Thankfully Apple corrected the signature a few years ago and now removes some of this confusion as it is defined as void objc_msgSend(void). @mikeash wrote a great description of it and I recommend it - see here.

This means that even if we decided to support __arglist as "varargs" it wouldn't help with the objc_msgSend example.

There are definitely use cases for native varargs and there is interest, but the statement "I can see a clear case of perfect becoming the enemy of good" isn't an accurate description of the nature of it being lower priority. The lower priority is based on the narrow cases where it is is needed - real varargs. There are the printf suite of functions but those really have little utility in a .NET application. Yes, there are cases where people do create and want to call Objective-C or C/C++ varargs functions from .NET and we acknowledge the work around is painful, but the interest just hasn't pushed this to the place where it is prioritized higher.

@TomEdwardsEnscape
Copy link

TomEdwardsEnscape commented Jun 17, 2023

I see, so __arglist implicitly inserts more than just the arguments passed to it? That is indeed not suitable for objc_msgSend, and I can also see the problems that could arise should multiple definitions of "variadic" exist on the same platform.

Solving the problem at its root would require something even lower level than __arglist, such as a similar keyword that inserted just the memory of the objects passed to it and left further decoration or padding to the delegate author. But this seems like an even bigger ask than the current proposals.

My opening statement wasn't describing the priority of the issue, I entirely understand why it's low. I was talking about how high-level parameter attributes were discussed for a long time, instead of starting with a low-level solution which unblocks the functionality and then refining/optimising it later. It was a bit frustrating to watch the years tick by.

(Aside: I'm interested to know how casting objc_msgSend works in native C/C++ on Arm64. Do you also have to insert the padding arguments that we see in C#? If not, how does the compiler know to put the arguments onto the stack instead of into registers?)

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Jun 17, 2023

I see, so __arglist implicitly inserts more than just the arguments passed to it?

It doesn't necessarily insert more, rather it instructs the JIT to pass the arguments in a certain way. That way conforms to the MSVC implementation of varargs and specifically how C++/CLI assumes they work. The details of how to pass floating point values via varargs is particularly treacherous on most platforms.

Solving the problem at its root would require something even lower level than __arglist, such as a similar keyword that inserted just the memory of the objects passed to it and left further decoration or padding to the delegate author.

That is definitely an option. However, the minimum needed is simply to have an indication for the JIT to know to pass the arguments using the varargs calling convention that is appropriate for the current platform - encoding all of that in the JIT is the real cost. There are some differences of opinion on precisely how one might want to do this. One approach is to provide a "placeholder" (that is, __arglist in C# or ... in C/C++). Another approach is to adorn the method with a calling convention that indicates to pass the arguments as they would be passed using varargs. The former makes the feature more natural from a language perspective but the latter is easier to express and handle in the runtime. A concrete example is helpful.

Using __arglist:

[DllImport(@"...")]
extern static void printf(string fmt, __arglist);

// Usage:
printf("%d", 5);
printf("%f", 3.14);

Use a new attribute:

[NativeVarargsAttribute(VarargBeginIndex = 1)]
[DllImport(@"...")]
extern static void printf(string fmt, int a);
[NativeVarargsAttribute(VarargBeginIndex = 1)]
[DllImport(@"...")]
extern static void printf(string fmt, double a);

// Usage:
printf("%d", 5);
printf("%f", 3.14);

I'm interested to know how casting objc_msgSend works in native C/C++ on Arm64.

That is the agreed upon ABI for varargs.

@TomEdwardsEnscape
Copy link

The attribute-plus-declared-parameters approach is certainly nice. But as I think objc_msgSend demonstrates, it's overly restrictive for some notable real-world use cases. A means to define the arguments at runtime, as __arglist already provides, is valuable.

Unless I misunderstand, the JIT needs to know not just which parts of the method should be variadic, but also which variadic convention should be used. To pose this as a question: does __arglist fail on Windows if the target method was compiled using GCC's libc?

Assuming yes, how about this proposal: multiple keywords, one per supported variadic convention.

extern static void printf(string fmt, argList_raw); // just pass as if normal arguments; would work for objc_msgSend
extern static void printf(string fmt, argList_msvc); // alias for the current __arglist
extern static void printf(string fmt, argList_libc); // other conventions added as deemed appropriate

MSVC support is obvious, and I include "raw" partly because it's what I need, but also because it's the simplest possible convention. Going further with libc etc. is up for debate.

If it's not practical to add multiple keywords, then the variadic convention can instead be declared in a method attribute.

Fully-typed signatures can still be achieved by wrapping the extern in a normal C# method:

[DllImport(@"...")]
extern static void printf(string fmt, argList_msvc);

static void printf(string fmt, double a) => printf(fmt, argList_msvc(a));

I'm sure this would be slower to execute than an extern decorated with NativeVarargsAttribute, but it's better to have an optimisation problem than a functionality problem. In this particular example the JIT knows exactly which types are being passed, so could in future use that information to optimise the call.

The biggest uncertainty I have is this: how much harder it is to support this approach in the runtime, compared to attributes?

That is the agreed upon ABI for varargs.

Good, so that can be hardcoded on Arm64 regardless of the variadic convention in use. One less language feature required!

@TheLastRar
Copy link

It also looks like the new attribute solution wouldn't handle the situation of reverse p/invoke well.
the argList_* solution looks at least at a glance better suited for reverse p/invoke,

How well does each solution handle a function defined with va_list instead of ...?
I guess you could just have an [DllImport(@"va_list")] like you have a [DllImport(@"...")] in the above example,
or maybe a [MarshalAs(UnmanagedType.VaList)] instead of a default of [MarshalAs(UnmanagedType.VariadicArguments)] on the argList_* type.

@ohroy
Copy link

ohroy commented Sep 9, 2024

any update ?

@am11
Copy link
Member

am11 commented Nov 11, 2024

Fun fact: on Windows this works (prints digit: 42, string: dotnet, character: ~, float: 42.420000) with net8.0:

using System.Runtime.InteropServices;

class C
{
    static void Main() =>
        printf("digit: %d, string: %s, character: %c, float: %f", __arglist(42, "dotnet", '~', 42.42));

    [DllImport("msvcrt")]
    static extern int printf(string format, __arglist);
}

but top-level statement generates a compilation error: Program.cs(6,41): error CS1669: __arglist is not valid in this context

using System.Runtime.InteropServices;

printf("digit: %d, string: %s, character: %c, float: %f", __arglist(42, "dotnet", '~', 42.42));

[DllImport("msvcrt")]
static extern int printf(string format, __arglist); // CS1669

@jkoritzinsky jkoritzinsky marked this as a duplicate of #82081 Dec 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.Runtime.InteropServices
Projects
Status: No status
Development

No branches or pull requests