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 struct
s 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.