Introduction
Many C# programmers are familiar with the Platform Invoke mechanism (using the DllImport attribute) for calling unmanaged functions from C#.
A problem that sometimes arises is calling a function that takes a variable number of arguments. Consider, for example, the sscanf() function in the C runtime library, which is defined as follows.
int sscanf(const char *str, const char *format, ...);
This function reads data from "str," splits it up using "format," and then stores the results in the locations pointed to by the remaining arguments (the "..."), which are variable in both number and type.
So, how can we call this function from C#?
First attempt
You may be thinking, no problem, C# can cope with a variable number of arguments using its "params" keyword, which leads you to try the following code.
using System;
using System.Text.RegularExpressions;
class Test
{
static void Main()
{
string str = "1 + 2 = three";
string pattern = @"(\d+) (\S) (\d+) (\S) (\S+)";
Match match = Regex.Match(str, pattern);
if (match.Success)
{
int firstNumber = int.Parse(match.Groups[1].Value);
char operation1 = char.Parse(match.Groups[2].Value);
int secondNumber = int.Parse(match.Groups[3].Value);
char operation2 = char.Parse(match.Groups[4].Value);
string resultString = match.Groups[5].Value;
Console.WriteLine($"First Number: {firstNumber}");
Console.WriteLine($"Operation 1: {operation1}");
Console.WriteLine($"Second Number: {secondNumber}");
Console.WriteLine($"Operation 2: {operation2}");
Console.WriteLine($"Result String: {resultString}");
}
else
{
Console.WriteLine("No match found.");
}
Console.ReadKey();
}
}
This builds fine, but then, when you try to run it, it blows up with a dreaded "System.AccessviolationException". So, what's wrong here?
Second attempt
The problem is that having consulted the "format" string, the C function is expected to be presented with 5-pointers in sequence. Instead, it gets a single pointer to an object array, a different animal.
This suggests that the following might work.
using System;
using System.Runtime.InteropServices;
using System.Text;
class Test
{
[DllImport("msvcrt.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
static extern int sscanf(string str, string format, out int arg1, out char arg2, out int arg3, out char arg4, StringBuilder arg5);
static void Main()
{
string str = "1 + 2 = three";
string format = "%d %c %d %c %s"; // int, char, int, char, string
int arg1, arg3;
char arg2, arg4;
StringBuilder arg5 = new StringBuilder(10); // need a buffer big enough to return the string
int result = sscanf(str, format, out arg1, out arg2, out arg3, out arg4, arg5);
Console.WriteLine("{0} {1} {2} {3} {4}", arg1, arg2, arg3, arg4, arg5);
Console.ReadKey();
}
}
And indeed, it does, reconstituting the original string.
1 + 2 = three
Suppose, though, that we have a dozen such strings to process, each with different formats and different types and numbers of variables. Does this mean that we have to write out a dozen overloads of the sscanf() function to use it from C#?
The third (and final) attempt
Fortunately, we can do this with just a single function declaration by making use of the obscure (and undocumented) C# keyword __arglist. Here's how.
using System;
using System.Runtime.InteropServices;
using System.Text;
class Test
{
[DllImport("msvcrt.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
static extern int sscanf(string str, string format, __arglist);
static void Main()
{
// First sscanf call
string str1 = "1 + 2 = three";
string format1 = "%d %c %d %c %s"; // int, char, int, char, string
int arg1, arg3;
char arg2, arg4;
StringBuilder arg5_1 = new StringBuilder(5); // buffer for string
int result1 = sscanf(str1, format1, __arglist(out arg1, out arg2, out arg3, out arg4, arg5_1));
// Second sscanf call
string str2 = "one + 2 = 3";
string format2 = "%s %c %d %c %d"; // string, char, int, char, int
StringBuilder arg5_2 = new StringBuilder(10); // buffer for string
int result2 = sscanf(str2, format2, __arglist(arg5_2, out arg2, out arg1, out arg4, out arg3));
// Output results
Console.WriteLine("First sscanf result: {0} {1} {2} {3} {4}", arg1, arg2, arg3, arg4, arg5_1);
Console.WriteLine("Second sscanf result: {0} {1} {2} {3} {4}", arg5_2, arg2, arg1, arg4, arg3);
Console.ReadKey();
}
}
This gives us the same result as before, plus an extra line.
one + 2 = 3
proving that only a single function declaration is needed. So the problem is solved.