在C#中使用P/Invoke调用64位系统上的sprintf及其相关函数

4

我在使用C#中的pinvoke调用_snwprintf时遇到了一个有趣的问题。它适用于整数类型,但不适用于浮点数。

这是在64位Windows上,32位上运行良好。

我的代码如下,请记住这只是一个编造的示例,以展示我所看到的行为。

class Program
{
    [DllImport("msvcrt.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
    private static extern int _snwprintf([MarshalAs(UnmanagedType.LPWStr)] StringBuilder str, IntPtr length, String format, int p);

    [DllImport("msvcrt.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
    private static extern int _snwprintf([MarshalAs(UnmanagedType.LPWStr)] StringBuilder str, IntPtr length, String format, double p);

    static void Main(string[] args)
    {
        Double d = 1.0f;
        Int32 i = 1;
        Object o = (object)d;
        StringBuilder str = new StringBuilder(32);

        _snwprintf(str, (IntPtr)str.Capacity, "%10.1lf", (Double)o);
        Console.WriteLine(str.ToString());

        o = (object)i;
        _snwprintf(str, (IntPtr)str.Capacity, "%10d", (Int32)o);
        Console.WriteLine(str.ToString());

        Console.ReadKey();
    }
}

这个程序的输出是:
   0.0
     1

第一行应该打印1.0而不是0.0,目前我被难倒了。


2
什么样的恶魔能够驱使你使用P/Invoke C运行时printf系列函数?C调用约定、可变参数、非操作系统部署的dll,这就像是酿造完美风暴! - Remus Rusanu
1
你不觉得 string.Format 已经足够好用了吗? - zneak
1
@zneak - 我很想使用 string.Format,而不是这个,但在这种情况下,我必须使用 printf 样式的格式说明符。如果这样行不通,我考虑的另一个解决方案是编写一个函数,将 printf 格式字符串转换为 C# 字符串,然后使用 string.Format。 - WildCrustacean
1
@所有人,我同意bde的观点,在这里我们应该尽力帮助他,当然前提是他知道自己在做什么。有时候你不能花费数千美元来重写整个应用程序,但我们需要足够聪明以节省时间来编写软件补丁。 - Akash Kava
这是一个有趣的问题,即使它不是许多人所需的东西。+1。 - Robert Fraser
显示剩余2条评论
5个回答

6
我不确定为什么你的调用不起作用,但这些方法的安全版本在x86和x64中都可以正常工作。
以下代码可以按预期工作:
class Program
{
    [DllImport("msvcrt.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
    private static extern int _snwprintf_s([MarshalAs(UnmanagedType.LPWStr)] StringBuilder str, IntPtr bufferSize, IntPtr length, String format, int p);

    [DllImport("msvcrt.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
    private static extern int _snwprintf_s([MarshalAs(UnmanagedType.LPWStr)] StringBuilder str, IntPtr bufferSize, IntPtr length, String format, double p);

    static void Main(string[] args)
    {
        // Preallocate this to a given length
        StringBuilder str = new StringBuilder(100);
        double d = 1.4;
        int i = 7;
        float s = 1.1f;

        // No need for box/unbox
        _snwprintf_s(str, (IntPtr)100, (IntPtr)32, "%10.1lf", d);
        Console.WriteLine(str.ToString());

        _snwprintf_s(str, (IntPtr)100, (IntPtr)32, "%10.1f", s);
        Console.WriteLine(str.ToString());

        _snwprintf_s(str, (IntPtr)100, (IntPtr)32, "%10d", i);
        Console.WriteLine(str.ToString());

        Console.ReadKey();
    }
}

那不起作用。如果我使用Single而不是Double,同样的问题会发生。不过我将更改示例以使用lf。请参阅主要注释,了解我为什么要这样做。 - WildCrustacean
@bde:我刚刚重新回答了这个问题。那里发布的代码在x86和x64上都可以正常工作,没有任何问题。 - Reed Copsey

4
可以使用未记录的 __arglist 关键字来实现:
using System;
using System.Text;
using System.Runtime.InteropServices;

class Program {
    [DllImport("msvcrt.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
    private static extern int _snwprintf(StringBuilder str, int length, String format, __arglist);

    static void Main(string[] args) {
        Double d = 1.0f;
        Int32 i = 1;
        String s = "nobugz";
        StringBuilder str = new StringBuilder(666);

        _snwprintf(str, str.Capacity, "%10.1lf %d %s", __arglist(d, i, s));
        Console.WriteLine(str.ToString());
        Console.ReadKey();
    }
}

请不要使用那个。


抱歉发了重复的帖子,没看到你的__arglist,当我回答时 :) - Alex F
我今天早些时候实际上尝试过那个,但是我无法让它工作。对我来说,所有这些示例代码只是打印格式字符串。也许我在这里漏掉了什么。虽然如此,我同意我不想使用这种方法。 - WildCrustacean
@bde:项目 + 属性,应用程序选项卡,平台目标 = x86。这就是风险所在。 - Hans Passant
1
那修复了,但这很奇怪。所以 __arglist 这个秘密的东西在 x64 上不受支持? - WildCrustacean
1
@bde:我的理解是__arglist无法正确获取x64调用约定。这是一个棘手的问题,因为前4个参数是通过寄存器传递的。 - Hans Passant

0

uint是32位的。snprintf的长度参数是size_t,在64位进程中为64位。将第二个参数更改为IntPtr,它是size_t的最接近的.NET等效项。

此外,您需要预先分配StringBuilder。目前您存在缓冲区溢出的问题。


尝试过了,问题还是一样。不过我会更新我的示例,应该使用IntPtr。 - WildCrustacean

0
尝试在第二个函数的最后一个参数上使用MarshalAs R8(这是真实/浮点8(即双精度))。

我刚刚阅读了 msdn 的帮助,期望的第二个参数是参数数组的形式,我不知道如何将其封送到 C 中的参数数组中,即使您将参数作为 double 发送,也许运行时不希望以相同的形式接收它,我不知道 int 是如何工作的,double 也应该能够正常工作。尝试将其作为双精度数组的第一项发送到最后一个参数,并尝试输入参数。 - Akash Kava
C和C++的变长参数与.NET参数数组在底层实现上非常不同。sprintf函数明显使用的是按值传递。 - Ben Voigt


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接