Search code examples
c++gomingw-w64cgo

How Call C++ Variables Using CGo For Standard Libraries


I am trying to get a variable value from a c++ code using cgo. For libraries ended in .hall works fine, but for libraries like <iostream>, <map>, <string> etc, I got the following error:

fatal error: iostream: No such file or directory
    4 | #include <iostream>
      |          ^~~~~~~~~~

Below my code:

package main

/*
#cgo LDFLAGS: -lc++
#include <iostream>
std::string plus() {
    return "Hello World!\n";
}
*/
import "C"
import "fmt"

func main() {

    a := Plus_go()
    fmt.Println(a)

}
func Plus_go() string {
    return C.plus()
}

I added the #cgo LDFLAGS: -lc++ flag because I saw this recommendation on an answer here on stackoverflow at https://stackoverflow.com/a/41615301/15024997.

I am using VS Code (not VS Studio), windows 10, Go 1.18 (lastest version).

I ran the following commands go tool cgo -debug-gcc mycode.go to trace compiler execution and output:

$ gcc -E -dM -xc -m64 - <<EOF

#line 1 "cgo-builtin-prolog"
#include <stddef.h> /* for ptrdiff_t and size_t below */

/* Define intgo when compiling with GCC.  */
typedef ptrdiff_t intgo;

#define GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; intgo n; } _GoString_;
typedef struct { char *p; intgo n; intgo c; } _GoBytes_;
_GoString_ GoString(char *p);
_GoString_ GoStringN(char *p, int l);
_GoBytes_ GoBytes(void *p, int n);
char *CString(_GoString_);
void *CBytes(_GoBytes_);
void *_CMalloc(size_t);

__attribute__ ((unused))
static size_t _GoStringLen(_GoString_ s) { return (size_t)s.n; }

__attribute__ ((unused))
static const char *_GoStringPtr(_GoString_ s) { return s.p; }
#line 3 "C:\\Users\\Home\\OneDrive\\Desktop\\DevicesC++\\devices.go"


#include <iostream>
std::string plus() {
    return "Hello World!\n";
}

#line 1 "cgo-generated-wrapper"
EOF
C:\Users\Home\OneDrive\Desktop\DevicesC++\devices.go:5:10: fatal error: iostream: No such file or directory
    5 | #include <iostream>
      |          ^~~~~~~~~~
compilation terminated.
C:\Users\Home\OneDrive\Desktop\DevicesC++\devices.go:5:10: fatal error: iostream: No such file or directory
    5 | #include <iostream>
      |          ^~~~~~~~~~
compilation terminated.

Solution

  • CGo allows you to link your Go code against code that implements the C-style foreign function interfaces. This does not mean that you can just stick arbitrary-language code into place.

    Let's start with the first problem, which is that the import "C" line in one of your Go files must contain only C code above it. That is:

    /*
    #include <stdlib.h>
    extern char *cstyle_plus();
    */
    

    is OK, but:

    /*
    #include <stdlib.h>
    extern std::string *plus();
    */
    

    is not, nor may you #include any C++ header here. To oversimplify things a bit, the comment here is in effect snipped out and fed to a C compiler. If it's not valid C, it won't compile.

    If you want to include C++ code, you can, but you must put it in a separate file or files (technically speaking, a "translation unit" in C or C++ terminology). CGo will then compile that file to object code.

    The next problem, however, is that the object code must conform to the C Foreign Function Interface implemented by CGo. This means your C++ code must return C types (and/or receive such types as arguments). As std::string is not a C string, you literally can't return it directly.

    It's not very efficient (and there exist some attempts to work around this), but the usual method for dealing with this is to have C functions return C-style "char *" or "const char *" strings. If the string itself has non-static duration—as yours does—you must use malloc here, specifically the C malloc (std::malloc may be a non-interoperable one).

    The function itself must also be callable from C code. This means we'll need to use extern "C" around it.

    Hence, our plus.cpp file (or whatever you would like to call it) might read this way:

    #include <stdlib.h>
    #include <iostream>
    
    std::string plus() {
            return "Hello World!\n";
    }
    
    extern "C" {
    char *cstyle_plus() {
            // Ideally we'd use strdup here, but Windows calls it _strdup
            char *ret = static_cast<char *>(malloc(plus().length() + 1));
            if (ret != NULL) {
                    strcpy(ret, plus().c_str());
            }
            return static_cast<char *>(ret);
    }
    }
    

    We can then invoke this from Go using this main.go:

    package main
    
    /*
    #include <stdlib.h>
    extern char *cstyle_plus();
    */
    import "C"
    import (
            "fmt"
            "unsafe"
    )
    
    func Plus_go() string {
            s := C.cstyle_plus()
            defer C.free(unsafe.Pointer(s))
            return C.GoString(s)
    }
    
    func main() {
            a := Plus_go()
            fmt.Println(a)
    }
    

    Adding a trivial go.mod and building, the resulting code runs; the double newline is because the C string has a newline in it, and fmt.Println adds a newline:

    $ go build
    $ ./cgo_cpp
    Hello World!
    
    

    This code is a bit sloppy: should malloc fail, it returns NULL, and C.GoString turns that into an empty string. However, real code should try, as much as possible, to avoid this kind of silly allocation-and-free sequence: we might know the string length, or have a static string that does not require this kind of silly malloc, for instance.