Использование pinvoke в С# для вызова sprintf и друзей на 64-битной

У меня возникла интересная проблема с использованием pinvoke в С# для вызова _snwprintf. Он работает для целых типов, но не для чисел с плавающей запятой.

Это на 64-битной винде, на 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, и пока я в тупике.


person WildCrustacean    schedule 19.03.2010    source источник
comment
Какой демон может заставить вас запустить семейство printf во время выполнения C? Соглашение о вызовах C, переменные аргументы, развернутые библиотеки DLL, отличные от ОС, это все равно, что приготовить идеальный шторм!   -  person Remus Rusanu    schedule 19.03.2010
comment
Разве string.Format недостаточно хорош для вас?   -  person zneak    schedule 19.03.2010
comment
@Remus - я понимаю, что это нехорошо делать и напрашивается на неприятности, однако иногда нам всем приходится жить с существующим кодом. Кроме того, как бы ужасно это ни было, мне все же кажется, что это должно работать.   -  person WildCrustacean    schedule 19.03.2010
comment
@zneak - я бы хотел использовать string.Format вместо этого, но в этом случае я должен использовать спецификаторы формата в стиле printf. Если это не сработает, другим решением, которое я рассматриваю, является написание функции для преобразования строк формата printf в строки С#, а затем использование string.Format.   -  person WildCrustacean    schedule 19.03.2010
comment
@bde: а, тогда понятно. Хотя преобразование строки формата в стиле printf в строку формата C# может сделать ее более переносимой, чем PInvoke.   -  person zneak    schedule 19.03.2010
comment
@everyone, я согласен с bde, здесь мы должны попытаться помочь ему, с условием, что, конечно, он знает, что делает, иногда вы не можете потратить тысячи долларов, чтобы переписать все приложение, но мы должны быть достаточно умными, чтобы сэкономить время, чтобы писать патчи в наше ПО.   -  person Akash Kava    schedule 19.03.2010
comment
Это интересная проблема, даже если это не то, что нужно многим людям. +1.   -  person Robert Fraser    schedule 19.03.2010


Ответы (5)


Я не совсем уверен, почему ваши звонки не работают, но защищенные версии этих методы работают корректно как в 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();
    }
}
person Reed Copsey    schedule 19.03.2010
comment
Это не работает. Та же проблема возникает, если я использую Single вместо Double. Однако я изменю пример, чтобы использовать lf. Смотрите основные комментарии, почему я пытаюсь это сделать. - person WildCrustacean; 19.03.2010
comment
@bde: я только что переделал свой ответ. Код, размещенный там, работает в x86 и x64 без проблем. - person Reed Copsey; 19.03.2010

Это возможно с недокументированным ключевым словом __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();
    }
}

Пожалуйста, не используйте это.

person Hans Passant    schedule 19.03.2010
comment
Извините за дублированный пост, не видел ваш __arglist, когда публиковал свой ответ :) - person Alex F; 19.03.2010
comment
Я на самом деле пробовал это ранее сегодня, и я не мог заставить его работать. Все, что делает этот пример кода для меня, это печатает строку формата. Может быть, я что-то упускаю здесь. Я согласен, что я бы предпочел не использовать этот метод. - person WildCrustacean; 19.03.2010
comment
@bde: проект + свойства, вкладка «Приложение», целевая платформа = x86. Таковы опасности. - person Hans Passant; 19.03.2010
comment
это исправило это, но это странно. Значит, секретный __arglist не поддерживается для x64? - person WildCrustacean; 19.03.2010
comment
@bde: моя интерпретация заключается в том, что __arglist не может правильно понять соглашение о вызовах x64. Это сложно, так как первые 4 аргумента передаются в регистрах. - person Hans Passant; 19.03.2010

uint 32 бита. Параметр длины snprintf равен size_t, что составляет 64 бита в 64-битных процессах. Измените второй параметр на IntPtr, который является ближайшим эквивалентом size_t в .NET.

Кроме того, вам необходимо предварительно выделить ваш StringBuilder. В настоящее время у вас переполнение буфера.

person Ben Voigt    schedule 19.03.2010
comment
Пробовал, та же проблема. Однако я обновлю свой пример, в любом случае он должен использовать IntPtr. - person WildCrustacean; 19.03.2010

Попробуйте MarshalAs R8 (это для реальных/плавающих 8 (что двойное)) для последнего параметра во второй функции.

person Akash Kava    schedule 19.03.2010
comment
Я только что прочитал справку msdn, второй ожидаемый параметр находится в форме массива параметров, я не знаю, как маршалировать это в массив параметров c, даже если вы отправляете свой параметр как двойной, возможно, среда выполнения не ожидает его в та же форма, интересно, как работает int, и поэтому double тоже должен работать. попробуйте отправить его как первый элемент в массиве двойных значений в последнем параметре и попробуйте указать параметры. - person Akash Kava; 19.03.2010
comment
Варарги C и C++ сильно отличаются от массивов параметров .NET. sprintf определенно использует передачу по значению. - person Ben Voigt; 19.03.2010

Взгляните на эти две статьи:

http://www.codeproject.com/Messages/2840231/Alternative-using-MSVCRT-sprintf.aspx (на самом деле это примечание к статье CodeProject)

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

person Alex F    schedule 19.03.2010