-1

I want to write function for C#, which will be similar to sprintf(3) and vsnprintf(3) functions in C (or similar to "format" command in Tcl). Currently I have some success with it, but still have a few questions.

Below is the source code. Class "Format" contains few public functions, which accepts variable arguments list (but no more than 7 arguments, see below) and produce formatted string. This is analog of sprintf(3) function. Also function "format" may accept C# tuple, which should hold format string in first element, and arguments for format string in other elements -- this is analog of vsprintf(3) function.

Unfortunately, here I have two major limitations:

1) I can't pass more than seven arguments to format() function, because arguments passed in C# tuple, and tuple can't have more than eight elements (first element is format string itself, this is needed because empty tuples unavailable in C#). I hope to obtain some suggestions, how can I improve my format() function to avoid this limitation.

2) Another major limitation, that not all types can be passed to C# template. Especially, I can't pass pointers to format function (in this case I will got following error: "error CS0306: The type 'int*' may not be used as a type argument"). This is C# limitation? It's possible to rewrite format() function(s) to avoid this limitation?

Another major inconvenience, is that I should open some particular library name, and use C function name, which is different for different operation systems:

  • for windows I should use "msvcrt.dll" and "_snprintf";

  • for linux I should use "libc.so.6" and "snprintf";

  • for linux on embedded platform C-library name might have different name...

It's possible to define which library should be opened at runtime, or extern functions marshalling might be determined only at compile time? So, I see two variants possible here:

1) I need produce different DLL's for different target platforms;

2) Or I can decide at runtime, which function name and libc-library should be used?

I not understood, how can I rewrite the code for first or second variant. Also it looks very inconvenient to have knowledge about libc name. Why just not call dlsym("snprintf") ?

Another question, I trying limit using of dynamic memory, but looks It's impossible to avoid allocating few String and StringBuilder classes on heap. This is looking scary, because when programming on C/C++ it's possible to do almost all work without dynamic memory allocation. May be somebody suggests me, how to improve format() functions to avoid dynamic memory allocation.

using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

namespace FK0
{
    using Args = ITuple;

    public class Format
    {
        // const string LIBC = "msvcrt.dll"; //"libc.so";
    // const string FUNC = "_snprintf";
        const string LIBC = "libc.so.6";
    const string FUNC = "snprintf";
        [DllImport(LIBC, EntryPoint=FUNC)] static private extern int snprintf(StringBuilder result, [MarshalAs(UnmanagedType.SysInt)] IntPtr size, StringBuilder format, [MarshalAs(UnmanagedType.I4)] int a1);
        [DllImport(LIBC, EntryPoint=FUNC)] static private extern int snprintf(StringBuilder result, [MarshalAs(UnmanagedType.SysInt)] IntPtr size, StringBuilder format, [MarshalAs(UnmanagedType.I8)] long a1);
        [DllImport(LIBC, EntryPoint=FUNC)] static private extern int snprintf(StringBuilder result, [MarshalAs(UnmanagedType.SysInt)] IntPtr size, StringBuilder format, double a1);
        [DllImport(LIBC, EntryPoint=FUNC)] static private extern int snprintf(StringBuilder result, [MarshalAs(UnmanagedType.SysInt)] IntPtr size, StringBuilder format, [MarshalAs(UnmanagedType.LPStr)]string a1);

        // function returns length of next format segment (string, copied as is, or single format spec.)
        static private int parse_format(string fmt, int pos)
        {
            int p = fmt.IndexOf('%', pos);
            if (p == -1) return fmt.Length - pos; // copy to end of string
            else if (p != pos) return p - pos;  // copy till %

            char[] fmt_term = {'d','i','o','u','x','X','e','E','f','F','g','G','a','A','c','s','p','n','%' };
            int e = fmt.IndexOfAny(fmt_term, p + 1);
            if (e == -1) throw new System.ArgumentException("invalid format string");
            return e - p + 1;  // format specifier length
        }

    // call real `snprintf(3)' from C-library, marshal arguments appropriately
        static private int call_snprintf(ref StringBuilder res, int len, StringBuilder fmt, Object arg)
        {
            if (arg is long || arg is ulong)
                return snprintf(res, (IntPtr)len, fmt, Convert.ToInt64(arg));
            else if (arg is float || arg is double || arg is decimal)
                return snprintf(res, (IntPtr)len, fmt, Convert.ToDouble(arg));
            else if (arg is string || arg is StringBuilder)
                return snprintf(res, (IntPtr)len, fmt, Convert.ToString(arg));
            else if (arg.GetType().IsPointer || arg is IntPtr)  // XXX can't pass pointer to template!!!
                return snprintf(res, (IntPtr)len, fmt, ((IntPtr)arg).ToInt64());
        //else if (arg.GetType()
            else
                return snprintf(res, (IntPtr)len, fmt, Convert.ToInt32(arg));
        }

    // vsnprintf-like function (accepts all arguments in tuple)
        static public string format(Args args)
        {
            if (! (args[0] is string))  // check, that first argument is string
                throw new System.ArgumentException("wrong string format type");

        // first pass
        // compute number of arguments, size of output string and max size of formatted output
            string fmt = args[0].ToString();
            StringBuilder ns = null, fs = new StringBuilder();
            int total_len = 0, maxlen = 0, narg = 1;
            int pos = 0;
            while (pos < fmt.Length) {
                int len = parse_format(fmt, pos);
                if (fmt[pos] == '%') { // pass format specifier to snprintf(3)
                    fs.Clear(); fs.Append(fmt, pos, len);
                    int flen = call_snprintf(ref ns, 0, fs, args[narg]);
                    if (flen == -1) throw new System.ArgumentException("wrong format string");
                    total_len += flen;
                    if (flen > maxlen) maxlen = flen;
                    narg++;
                }
                else { // compute size of literal part
                    total_len += len;
                }
                pos += len;
            }

            if (narg != args.Length)
                throw new System.ArgumentException("incorrect # of arguments for format string");

        // second pass
        // print each argument separately
            var result = new StringBuilder(total_len);
            var part = new StringBuilder(maxlen + 1);  // include terminating zero
            pos = 0; narg = 1;
            while (pos < fmt.Length) {
                int len = parse_format(fmt, pos);
                if (fmt[pos] == '%') { // pass format specifier to snprintf(3)
                    fs.Clear(); fs.Append(fmt, pos, len);
                    call_snprintf(ref part, part.Capacity, fs, args[narg++]);
                    result.Append(part);
            Console.WriteLine(part);
                }
                else { // copy literal part as is
                    result.Append(fmt, pos, len);
                }
        pos += len;
            }

            return result.ToString();
        }

    // C# have no vararg templates, as C++03, also max size of tuple limited to 8 elements,
    // also impossible to create empty tuple, so maximum number arguments limited to 7 (plus format string as 0-th element).

        static public string format<T1, T2, T3, T4, T5, T6, T7>(string fmt, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6, T7 a7)
        {
            return format(Tuple.Create(fmt, a1, a2, a3, a4, a5, a6, a7));
        }

        static public string format<T1, T2, T3, T4, T5, T6>(string fmt, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5, T6 a6)
        {
            return format(Tuple.Create(fmt, a1, a2, a3, a4, a5, a6));
        }

        static public string format<T1, T2, T3, T4, T5>(string fmt, T1 a1, T2 a2, T3 a3, T4 a4, T5 a5)
        {
            return format(Tuple.Create(fmt, a1, a2, a3, a4, a5));
        }

        static public string format<T1, T2, T3, T4>(string fmt, T1 a1, T2 a2, T3 a3, T4 a4)
        {
            return format(Tuple.Create(fmt, a1, a2, a3, a4));
        }

        static public string format<T1, T2, T3>(string fmt, T1 a1, T2 a2, T3 a3)
        {
            return format(Tuple.Create(fmt, a1, a2, a3));
        }

        static public string format<T1, T2>(string fmt, T1 a1, T2 a2)
        {
            return format(Tuple.Create(fmt, a1, a2));
        }

        static public string format<T1>(string fmt, T1 a1)
        {
            return format(Tuple.Create(fmt, a1));
        }

        static public string format(string fmt)
        {
            return format(Tuple.Create(fmt));
        }
    };
}

public class Program
{
    unsafe public static void Main()
    {
    //  System.Threading.Thread.Sleep(100000);
        int z = 123;
        int* y = &z;
        IntPtr v = (IntPtr)y;
        string s = FK0.Format.format("%p %d %d", v, *y, z);
        Console.WriteLine(s);
    }
}
Kirill Frolov
  • 401
  • 4
  • 10
  • 4
    Is there a particular reason you want to use this rather than the existing C# capabilities like `string.Format`? – mjwills Nov 09 '18 at 11:49
  • Similar question: https://stackoverflow.com/questions/28155317/what-is-the-printf-in-c-sharp – Kirill Frolov Nov 09 '18 at 11:58
  • Besides this huge amount of work that could be easily done with a `string.Format` call as @mjwills said, you can always create a `public class Tuple` if you want. It won't solve the problem, though – Camilo Terevinto Nov 09 '18 at 11:58
  • 1
    There's also a (undocumented?) keyword called `__arglist` which can be used to call varargs functions like `printf`. But I would recommend sticking to `String.Format` for code that's in a production environment. – Dirk Nov 09 '18 at 12:04
  • use string.Format instead of writing the whole bunch of codes. – Prakash Nov 09 '18 at 12:13
  • Similar question: https://stackoverflow.com/questions/29675018/marshalling-argiterator-to-va-list – Kirill Frolov Nov 09 '18 at 12:26
  • @PetSerAl You can define `printf` as `extern int printf(string format, __arglist)` using *msvcrt.dll* and call it using `printf("Hello %s!", __arglist("World"));`. – Dirk Nov 09 '18 at 12:33
  • @Dirk Was unaware of right syntax for calling varargs functions from C#. Definitely not how them called in C/C++. – user4003407 Nov 09 '18 at 13:03
  • @mjwills The question is above: how to write printf-like function. I _don't_ _asking_ _how_ _to_ _perform_ _formatting_ operation (this will be completely different question, I know, what exists many possibilities to print something from C#). I exactly want to write printf function and asking for suggestions (see my questions) how to improve it. – Kirill Frolov Nov 09 '18 at 13:27
  • Unfortunately, __arglist is unusable (need special/different syntax to call function with arglist). Also I still need marshall different types differently (follow integer promotion rule for C), I can't rely on C# in that. So every parameter to printf I should pass manually. Marshalling whole __arglist is not an option. – Kirill Frolov Nov 09 '18 at 13:32
  • Is there some reason you can't use `format(params object[] args)` and then work through the types of `ps`? Or perhaps more vsnprintf like, `format(out string ans, string format, params object[] args)` ? – NetMage Nov 09 '18 at 21:06
  • Note the C# `ValueTuple` handles more than 7 arguments by chaining another `ValueTuple` in the `Rest` field. – NetMage Nov 09 '18 at 21:17
  • 1
    There are like half a dozen questions in here, and the entire enterprise seems like a bad idea in the first place; if you want to call printf, just call the C++/CLI version of printf! Can you break this out into **one simple question per question** where each question has a clear, specific answer? – Eric Lippert Nov 09 '18 at 21:49

1 Answers1

2

Using the C# params parameter modifier, you can have the arguments to a method passed as an array:

public static string format(string format, params object[] args) {
    var n = 0;
    return String.Format(format.Replace(new Regex(@"%[+-0-9.]*[a-z]"), m => $"{{{n++}}}"), args);
}

Now you can call it like:

Console.WriteLine(Format.format("test: %s %x", "this", 23));

Of course, this version of format just dumps all arguments in their default format, you would need to process each format specifier in a real implementation.

NetMage
  • 26,163
  • 3
  • 34
  • 55