tags:

views:

316

answers:

5

I am having an interesting problem with using pinvoke in C# to call _snwprintf. It works for integer types, but not for floating point numbers.

This is on 64-bit Windows, it works fine on 32-bit.

My code is below, please keep in mind that this is a contrived example to show the behavior I am seeing.

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();
    }
}

The output of this program is

   0.0
     1

It should print 1.0 on the first line and not 0.0, and so far I am stumped.

A: 

uint is 32 bits. The length parameter of snprintf is size_t, which is 64 bits in 64 bit processes. Change the second parameter to IntPtr, which is the closest .NET equivalent of size_t.

In addition, you need to preallocate your StringBuilder. Currently you have a buffer overrun.

Ben Voigt
Tried that, same problem. I'll update my example though, it should be using IntPtr anyway.
bde
+2  A: 

I'm not exactly sure why your calls do not work, but the secured versions of these methods do work properly in both x86 and x64.

The following code does work, as expected:

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();
    }
}
Reed Copsey
That doesn't work. The same problem happens if I use Single instead of Double. I'll change the example to use lf though. See the main comments for why I am trying to do this.
bde
@bde: I just redid my answer. The code posted there works in x86 and x64, with no issues.
Reed Copsey
Thanks, that works for me too.
bde
+1 for finding a solution to that. You deserve it.
zneak
A: 

Try MarshalAs R8 (Thats for real/floating 8 (which is double)) on last parameter in second function.

Akash Kava
Tried that too, same result.
bde
I just read msdn help, the 2nd parameter expected is in form of parameter array, I am not aware of how to marshal this to c's parameter array, even if you are sending your parameter as double, may be runtime is not expecting it in the same form, i wonder how int worked and so does double should work too. try sending it as first item in array of double in last parameter and try puting params.
Akash Kava
C and C++ varargs are very different underneath from .NET parameter arrays. sprintf definitely uses pass-by-value.
Ben Voigt
+1  A: 

It is possible with the undocumented __arglist keyword:

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();
    }
}

Please don't use that.

Hans Passant
Sorry for duplicated post, didn't see your __arglist when posted my answer :)
Alex Farber
I actually had tried that earlier today, and I couldn't get it to work. All this sample code does for me is print the format string. Maybe I am missing something here. I agree that I'd rather not use this method though.
bde
@bde: Project + Properties, Application tab, Platform Target = x86. Such are the hazards.
Hans Passant
@nobugz: that fixed it, but that's bizarre. So the secret __arglist thing isn't supported for x64?
bde
@bde: my interpretation is that __arglist can't get the x64 calling convention right. It is a tricky one since the first 4 arguments are passed in registers.
Hans Passant
A: 

Take a look at these two articles:

http://www.codeproject.com/Messages/2840231/Alternative-using-MSVCRT-sprintf.aspx (this is actually note to CodeProject article)

http://bartdesmet.net/blogs/bart/archive/2006/09/28/4473.aspx

Alex Farber