Search code examples
golinkercgo

How to resolve circular dependencies when using go modules and cgo


In my project, I am using callbacks for bi-directional calls from C into go and vice versa using CGO. I resolved the issue of circular dependencies by compiling the C part into a library, then compiling the go part into a library, then a final linker pass puts it all together. This is working fine when not using go modules. Go source files are listed on the command line explicitly. I have been told that as of go 1.12 "this is not the right way to do it".

As the project has grown, I now want to use go modules. Unfortunately, this changes the behaviour of the go compiler. It now wants to resolve external dependencies and implicitly includes them in the output file. Due to the circular dependency, it now always ends up with an undefined reference or multiple definitions. How to resolve circular dependencies when using cgo and go modules "the right way"?

This is a minimal example to illustrate the problem. Remove the file-name "hello.go" from the call to go in the Makefile to see how it falls apart.

This is the error message:

hello.c:3: multiple definition of `c_hello'; $WORK/b001/_cgo_hello.o:/tmp/go-build/hello.c:3: first defined here

Makefile:

libchello.a: Makefile hello.c
    gcc -fPIC -c -o chello.o hello.c
    ar r libchello.a chello.o
libgohello.a: Makefile hello.go libchello.a
    env CGO_LDFLAGS=libchello.a go build -buildmode=c-archive -o libgohello.a hello.go
main: Makefile main.c libgohello.a libchello.a
    gcc -o main main.c libchello.a libgohello.a -pthread
.PHONY: clean
clean:
    rm -f main *.a *.o
    echo "extern void go_hello();" > libgohello.h

hello.go:

package main
/*
extern void c_hello();
*/
import "C"
import "time"
import "fmt"
//export go_hello
func go_hello() {
    fmt.Printf("Hello from go\n")
    time.Sleep(1 * time.Second)
    C.c_hello()
}
func main() {}

libgohello.h:

extern void go_hello();

hello.c:

#include "libgohello.h"
#include <stdio.h>
void c_hello() {
    printf("Hello from c\n");
    go_hello();
}

main.c:

void c_hello();
int main() {
    c_hello();
}

go.mod:

module hehoe.de/cgocircular

Solution

  • If you look at the verbose output from the go build command, you will see that when compiling the directory as a complete go package, the main.c file is being included as part of the C code used in hello.go.

    From the documentation:

    When the Go tool sees that one or more Go files use the special import "C", it will look for other non-Go files in the directory and compile them as part of the Go package

    The easiest solution here is to separate the main C and Go packages, so that they don't interfere with each other's build process. Testing this out, removing the main.c file will build libchello.a and libgohello.a, and then adding it back in will complete the build of main.