Calling Go Functions from C: Implementation Steps and Underlying Mechanism

This article explains how to call Go functions from C within the same process, covering the three‑step implementation, compilation into a shared library, the C invocation code, and a detailed walkthrough of the cgo and Go runtime mechanisms that make cross‑language calls possible.

IT Services Circle
IT Services Circle
IT Services Circle
Calling Go Functions from C: Implementation Steps and Underlying Mechanism

In this article we explore whether a language can invoke functions implemented in another language within the same process, using C calling Go as a concrete example.

1. C calling Go function example

The implementation consists of three steps:

Define and implement a function in Go.

Compile the Go code into a static or dynamic library.

Call the library from C.

1.1 Go function definition and implementation

package main

//int add(int a, int b);
import "C"

//export add
func add(a, b C.int) C.int {
    return a + b
}

func main() {}

Key points:

The line import "C" enables cgo, causing go build to invoke gcc.

The comment //int add(int a, int b) is not a Go comment but a C declaration. //export add makes the function visible to external callers.

Parameters must use the C types provided by cgo (e.g., C.int).

1.2 Compiling Go code into a library

# go build -buildmode=c-shared -o libadd.dylib main.go

The flag -buildmode=c-shared produces a dynamic library ( .dylib on macOS, .so on Linux); -buildmode=c-archive would produce a static archive.

1.3 C code calling the library

#include <stdio.h>
#include "libadd.h"

int main(void) {
    int ret = add(2, 3);
    printf("C调用Go函数2+3= %d
", ret);
    return 0;
}

Compile and link the C program with the generated library: # gcc main.c -L. -ladd -o main Running the executable prints C调用Go函数2+3= 5, confirming the successful cross‑language call.

2. Underlying mechanism of C calling Go

2.1 cgo compilation tool

Running go tool cgo main.go generates intermediate files such as main.cgo1.go, main.cgo2.c, _cgo_gotypes.go, and the export files _cgo_export.c and _cgo_export.h inside the _obj directory.

2.2 Function entry in _cgo_export.c

int add(int a, int b) {
    __SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done();
    typedef struct {
        int p0;
        int p1;
        int r0;
    } __attribute__((__packed__)) _cgo_argtype;
    static _cgo_argtype _cgo_zero;
    _cgo_argtype _cgo_a = _cgo_zero;
    _cgo_a.p0 = a;
    _cgo_a.p1 = b;
    crosscall2(_cgoexp_ec46b88da812_add, &_cgo_a, 12, _cgo_ctxt);
    return _cgo_a.r0;
}

This wrapper ensures the Go runtime is initialized and then forwards the call to crosscall2, packing the arguments into a struct.

2.3 Go runtime crosscall2

//file:runtime/cgo/asm_amd64.s
TEXT crosscall2(SB),NOSPLIT,$0-0
    PUSH_REGS_HOST_TO_ABI0()
    ADJSP $0x18
    MOVQ CX, 0x0(SP)   // fn
    MOVQ DX, 0x8(SP)   // arg
    MOVQ R9, 0x10(SP)  // ctxt
    CALL runtime·cgocallback(SB)
    ADJSP $-0x18
    POP_REGS_HOST_TO_ABI0()
    RET

The function saves caller registers, prepares the stack, and invokes runtime·cgocallback, which switches from the C thread stack to a Go goroutine stack and locks the OS thread.

2.4 Go stub generated in _cgo_gotypes.go

//go:cgo_export_dynamic add
//go:linkname _cgoexp_ec46b88da812_add _cgoexp_ec46b88da812_add
//go:cgo_export_static _cgoexp_ec46b88da812_add
func _cgoexp_ec46b88da812_add(a *struct{p0 _Ctype_int; p1 _Ctype_int; r0 _Ctype_int}) {
    a.r0 = add(a.p0, a.p1)
}

This stub converts the packed C arguments back to Go types and calls the user‑defined add function:

//export add
func add(a, b C.int) C.int {
    return a + b
}

3. Summary

The cross‑language call involves three cooperating parts: the user code (Go function and C caller), the cgo‑generated stub code, and the Go runtime (functions like crosscall2, cgocallback, and related helpers) that handle register saving, stack switching, and thread locking.

The first entry point is the stub in _cgo_export.c.

The Go runtime then performs ABI conversion, stack switching, and invokes the actual Go function.

After runtime processing, the call returns through the generated stub back to the C side.

Compared with ordinary in‑process function calls, cross‑language calls incur additional overhead due to runtime coordination, but they remain faster than RPC because they avoid kernel‑level protocol handling.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

InteroperabilitycgoCross-language
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.