1

I'm compiling a C library from GO code, using CGO. The libraries functions are then called from C#.

In this GO code I have a function that expects a []string input, such as: func StringArray(strings []string)

I also have another function that expects an []int input, such as: func IntArray(vals []int)

If I look at the generated header file, I can see the following for the above functions:

extern __declspec(dllexport) void IntArray(GoSlice vals);
extern __declspec(dllexport) void StringArray(GoSlice strings);

I can successfully call the IntArray function from C#, by creating the following struct:

internal struct GoSlice
{
    public IntPtr data;
    public long len, cap;
    public GoSlice(IntPtr data, long len, long cap)
    {
        this.data = data;
        this.len = len;
        this.cap = cap;
    }
}

And then call the function like so:

long[] data = { 1, 2, 3, 4, 5, 6 };
IntPtr data_ptr = Marshal.AllocHGlobal(Buffer.ByteLength(data));
Marshal.Copy(data, 0, data_ptr, data.Length);
var nums = new GoSlice(data_ptr, data.Length, data.Length);
IntArray(nums);
Marshal.Copy(nums.data, data, 0, data.Length);

I can also successfully call functions expecting a string input, by creating the following struct:

internal struct GoString
{
    public string msg;
    public long len;
    public GoString(string msg, long len)
    {
        this.msg = msg;
        this.len = len;
    }
}

And then just call the function like so:

string inputString = "Test";
GoString goString = new GoString(inputString, inputString.Length);

StringInput(goString);

What I struggle to achieve, is to pass the expected []string GoSlice to the StringArray function. Any suggestions? I need the GoSlice to include strings and not integers.

I've tried, in various ways, to pass strings to the GoSlice instead of integers which didn't work with mixed results. I expected to end up with a []string GoSlice which could be used when calling the "CGO compiled" GO function from C#.

user8973449
  • 175
  • 1
  • 11

2 Answers2

1

I am a little confused about when you are referring to C# or Go types in your questions, but I think i can still clear this up for you.

Firstly, cgo creates a C interface so your problem is simplified to:

  • How can I call a C function that takes an array from C#
  • How can I accept a C array from a cgo interface in GO

Seems like you have a good grasp on the former, so i am going to focus on the later.

For C functions with dynamic structures, we need to know how much memory and the layout of it. So a function that accepts an array of strings (containing the same type of data as a String Slice) could look like the following in C

int Parse22Strings(int argc, char** argv){
   if(argc!=2){
      return -1;
   }
   printf("string #1 %s string #2\n",argv[0],argv[1]);
}

Ok so if we want the same interface using Go, we just have to match it in cgo (taken form this other answer):

func Parse22Strings(argc C.int, argv **C.char) {

    length := int(argc)
    tmpslice := (*[1 << 30]*C.char)(unsafe.Pointer(argv))[:length:length]
    gostrings := make([]string, length)
    for i, s := range tmpslice {
        gostrings[i] = C.GoString(s)
    }

    fmt.Printf("string #1 %s string #2\n",gostrings[0],gostrings [1]);
}
    

So at this point you can just treat the function above as int Parse22Strings(int argc, char** argv) because that is how it will be called. If you need to return a Slice you just need to again transform it to a C type:

struct GoSliceSimple{
  int argc;
  char ** argv;
};

func Parse22Strings(argc C.int, argv **C.char) C.struct_GoSliceSimple {
//be sure to use malloc so the Go garbage collector does not destroy any returned string
}

You can use can more complicated with opaque types, but generally just serializing the data in C structs and passing it back and forth is the best way to go.

Liam Kelly
  • 3,524
  • 1
  • 17
  • 41
  • Thank you Liam for taking the time and answer my question. I'm sorry if I'm a bit confusing in my explanation, this is a bit of a struggle since it involves a lot of new topics for me. However, the examples I have above are just copied from other places, i.e. I'm not familiar calling C functions that takes an array from C# (or calling C functions from C# at all for that matter). Would I be to bold asking you for an example, on how to call the GO (cgo interface) function in your example, from C#? – user8973449 Feb 27 '23 at 10:14
  • You can find a very complete answere here: https://stackoverflow.com/a/11593657/1987437 – Liam Kelly Feb 27 '23 at 13:29
  • Ok, so I'm calling the Parse22Strings function from C# like so: [DllImport("shared.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] internal static extern void Parse22Strings(int argc, string[] argv); string[] strArray = {"one", "two", "three", "four", "five", "six", "seven"}; Parse22Strings(strArray.Length, strArray); But the result I get back is: o t t f f s s. So it seems like I only get the first character of each string, do you have any advice? – user8973449 Mar 01 '23 at 10:10
  • I managed to fix that, so now it's working! I changed the CharSet to the default of Ansi. I guess this has to do with the terminating null character in the different CharSet. – user8973449 Mar 01 '23 at 10:37
0

Thanks to Liams input, I managed to come up with the following solution.

Exported GO function:

//export Parse22Strings
func Parse22Strings(argv **C.char, argc C.int) {
    length := int(argc)
    tmpslice := unsafe.Slice(argv, length)
    gostrings := make([]string, length)
    for i, s := range tmpslice {
        gostrings[i] = C.GoString(s)
    }
}

Since I'm using version 1.20.1 of GO, I actually changed:

tmpslice := (*[1 << 30]*C.char)(unsafe.Pointer(argv))[:length:length]

To:

tmpslice := unsafe.Slice(argv, length)

As per the documentation from the CGO wiki here: https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices

C# Platform Invoke (P/Invoke):

[DllImport("shared.dll", CallingConvention = CallingConvention.Cdecl)]
internal static extern void Parse22Strings(string[] argv, int argc);

C# library call:

string[] strArray = {"one", "two", "three", "four", "five", "six", "seven", "..."};
Parse22Strings(strArray, strArray.Length);
user8973449
  • 175
  • 1
  • 11