Cで書かれたDLL内にある、可変引数をとる関数をC#から呼び出したいとき、__arglistを使えばよいことらしいことは分かりました。たとえばこんなページを発見。

だがしかし、呼び出し側に __arglist(1, 2)といったことを書かせるのは、自分だけでコードを書いている場合はともかく、ラッパを書く場合にはちょっといただけないですね。しかしながら、普通にC#の枠内にいる限り、これはどうしようもないらしい。

C#で書けなければILで書けばいいじゃない。という訳で…

まずは ildasmで調査。(ildasm.exeはVisual Studioに標準で入ってました。いい世の中です)

まずは__arglistがある呼び出しはどうなるのか。
C#でのメソッド。(_snwprintf は DllImport付きで宣言済み)
        static void Test1()
{
char[] buf = new char[100];
int r = _snwprintf(buf, buf.Length,
"test1: %d, %d", __arglist(10, 20));
String s = new string(buf, 0, r);
Console.WriteLine(s);
}
これをコンパイルするとこんな感じ。
.method private hidebysig static void  Test1() cil managed
{
// コード サイズ 45 (0x2d)
.maxstack 5
.locals init ([0] char[] buf,
[1] int32 r,
[2] string s)
IL_0000: nop
IL_0001: ldc.i4.s 100
IL_0003: newarr [mscorlib]System.Char
IL_0008: stloc.0
IL_0009: ldloc.0
IL_000a: ldloc.0
IL_000b: ldlen
IL_000c: conv.i4
IL_000d: ldstr "test1: %d, %d"
IL_0012: ldc.i4.s 10
IL_0014: ldc.i4.s 20
IL_0016: call vararg int32 PrintTest.Program::_snwprintf(char[],
int32,
string,
...,
int32,
int32)
IL_001b: stloc.1
IL_001c: ldloc.0
IL_001d: ldc.i4.0
IL_001e: ldloc.1
IL_001f: newobj instance void [mscorlib]System.String::.ctor(char[],
int32,
int32)
IL_0024: stloc.2
IL_0025: ldloc.2
IL_0026: call void [mscorlib]System.Console::WriteLine(string)
IL_002b: nop
IL_002c: ret} // end of method Program::Test1

どうやら、callの仕方が変更されるようです。(初めて真面目にILを眺めたのですがLua等と同じスタックマシンなのですね。あ、比べるべきはJavaVMか)

ではこれを実現するべくこんな感じで。
using System;
using System.Collections.Generic;
using System.Text;

using System.Runtime.InteropServices;
using System.Reflection;
using System.Reflection.Emit;

namespace PrintTest
{
 class Program
{
static void Main(string[] args)
{
Console.WriteLine(SPrintf(100,
"print is working?: %d, %s, %f", 999, "str", 0.3));
}

static string SPrintf(int bufsize, string fmt, params object[] args)
{
char[] buf = new char[bufsize];

DynamicMethod meth = new DynamicMethod(
"_snwprintf_invoker",
typeof(int),
new Type[] { buf.GetType() },
typeof(Program));

MethodInfo mi = typeof(Program).GetMethod(
"_snwprintf",
BindingFlags.Static | BindingFlags.NonPublic);

ILGenerator il = meth.GetILGenerator();

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, buf.Length);
il.Emit(OpCodes.Ldstr, fmt);

List<Type> paramTypes = new List<Type>();
foreach (var a in args)
{
if (a is int)
{
il.Emit(OpCodes.Ldc_I4, (int)a);
paramTypes.Add(typeof(int));
}
else if (a is string)
{
il.Emit(OpCodes.Ldstr, (string)a);
paramTypes.Add(typeof(string));
}
else if (a is double)
{
il.Emit(OpCodes.Ldc_R8, (double)a);
paramTypes.Add(typeof(double));
}
else
throw new ArgumentException("argument type is not supported");
}

il.EmitCall(OpCodes.Call, mi, paramTypes.ToArray());
il.Emit(OpCodes.Ret);

int r = (int)meth.Invoke(null, new object[] { buf });

return new string(buf, 0, r);
}

[DllImport("msvcrt.dll",
CallingConvention = CallingConvention.Cdecl,
CharSet=CharSet.Unicode)]
private static extern int
_snwprintf([Out] char[] buf, int size, string fmt, __arglist);
}
}

EmitCallがカギのようです。ここに与える引数は、__arglist(ここ)の中に入れるものだけで、関数の引数全部ではないです。(ハマったので)

printf族だと可変引数の型が一定でないのでコードが長くなってしまいますが、文字列だけ、とか数字だけの引数が続くのであればもっとシンプルになる かと思われます。

今回ILというハンマーを手に入れた訳ですが、何でも釘に見えないように気をつけなくては。