1

I have a shared library built with Go and a program in C# that I want to call the function in the shared library. But prints an empty line.

This is the C# code:

using System.Runtime.InteropServices;

class Program
{
    [DllImport("./main.so", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    static extern void Println(string s);

    static void Main(string[] args)
    {
        Println("hello world");
    }
}

This the Go code:

package main

import "C"
import "fmt"

//export Println
func Println(s string){
    fmt.Println(s)
}

func main(){}

How I can pass the C# string as a Go string?

kostix
  • 51,517
  • 14
  • 93
  • 176
johnman
  • 13
  • 3

1 Answers1

1

The problem

The problem is that while -buildmode=c-shared (and -buildmode=c-whatever in general) do indeed make the Go symbols callable from C, this comes with a twist when it comes to strings.

In C, there really is no such thing as a string but a convention exists that a string is a pointer to its first byte, and the length of a string is implicitly defined by a byte with code 0 (ASCII NUL) in that string.

In Go, strings are structs of two fields: a pointer to a memory block containing the string's contents and the number of bytes in that block.

As you can see, when the Go toolset compiles a Go function marked as "exported to C", it basically has two choices:

  • Make the function callable "as is"—requiring the callers to pass a Go-style struct value describing a string.
  • Artifically "wrap" the exported function into a code block which would take a "C-style" NUL-terminated string, count the number of bytes in it, cook a Go-style struct value and finally call the original function.

The second approach could arguably be simpler for non-Go programmers to deal with but there exist two arguments against using it:

  • This approach incurs hidden cost for each call: scanning the bytes of the string—even if the caller knows it.
  • It's possible—if needed—to create such a wrapper right in the Go code—as @Selvin suggested in their comment to the question.

So, to reiterate, when the Go toolset compiles a Go function marked as "exported to C" in a c-whatever mode, it follows the Go convention and the result of its work is:

  • The compiled library expects to receive a struct-typed value comprised of a pointer and a size—as described above—for each argument of Go type string of each exported function.
  • The supporting C header file is be generated, containing the definition of that struct type—it will be called GoString,—and a declaration for all your exported functions, using that GoString type for the arguments which are string on the Go side.

To say it in more simple words, if you were to create a file foo.go containing

package main

import "C"

import "fmt"

//export PrintIt
func PrintIt(string s) {
    fmt.Println(s)
}

and were then compile it using go build -buildmode=c-shared -o foo.so foo.go, the Go toolset would create both foo.so and foo.h, containing—among other things—somethig like:

typedef struct { const char *p; ptrdiff_t n; } GoString;

extern void PrintIt(GoString p0);

As you can see, to call PrintIt, you're supposed to pass it an instance of GoString, not a value of type const char *.

A solution

A proper solution is to have something like

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack=0)]
struct GoString {
    byte *p;
    int  n;
}

And then

[DllImport("./main.so", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
static extern void Println(GoString gs);

Note that I'm not quite sure plain int is OK to be used for ptrdiff_t in each and every case—please do your own research on what should be properly be used for in in .NET interop.

kostix
  • 51,517
  • 14
  • 93
  • 176
  • On a side note, it _may_ be useful to keep in mind that in some circumstances Go _assumes_ its strings are encoded in UTF-8—such as when iterating over a string using the `for ... range` statement; so you might consider specifying another charset for the interop if you're supposed to pass strings which may contain Unicode characters outside of the ASCII subset. – kostix Jan 05 '20 at 19:56
  • 2
    you may replace `byte *p;` with `string p;` with `MarshalAs(UnmanagedType.LPUTF8Str)` attribute – Selvin Jan 05 '20 at 22:00