Question

Calling C++ from Go with cgo and C Wrappers

gocppwrapper

Question

In Go, how can I call existing C++ code and wrap C++ classes so they can be used from Go?

Short Answer

Go can call native code with cgo, but cgo is designed around the C ABI rather than C++ classes. You will learn how to expose a small C-compatible API around a C++ class, represent the class as an opaque pointer in Go, manage object lifetime safely, and build the Go and C++ code together.

Concept

Go does not directly understand C++ language features such as classes, method overloading, templates, exceptions, or C++ name mangling. However, Go's cgo feature can call functions that use the stable, broadly supported C application binary interface (ABI).

The usual solution is therefore a two-layer design:

  1. Keep the real implementation in C++.
  2. Write a small wrapper with functions declared as extern "C".
  3. Call those wrapper functions from Go using cgo.

An extern "C" declaration tells the C++ compiler to export a function using C linkage. This prevents C++ name mangling and gives cgo a simple function-based interface to call.

Because a C++ object is not a C type, the wrapper normally returns it as an opaque handle: a void* pointer. Go stores that pointer but does not inspect or modify the C++ object directly. All operations happen through wrapper functions.

This separation matters because it creates a small, explicit boundary between Go and C++. It also makes memory ownership, error reporting, and API compatibility easier to control.

Mental Model

Think of a C++ class as a machine in a locked workshop.

  • The C++ code owns the machine and knows its internal parts.
  • The C wrapper is the workshop service desk.
  • Go receives a claim ticket—an opaque pointer—not the machine itself.
  • Go asks the service desk to create the machine, perform operations, and dispose of it.

Go should never try to open the workshop door and manipulate the C++ object's memory directly. It should only use the wrapper functions provided by the C++ side.

Take Quiz

Syntax and Examples

A C++ wrapper exposes plain functions with C linkage. A common pattern is create, operation functions, and destroy.

// counter_api.h
#pragma once

#ifdef __cplusplus
extern "C" {
#endif

void* counter_create(int initial_value);
int counter_increment(void* handle);
int counter_value(const void* handle);
void counter_destroy(void* handle);

#ifdef __cplusplus
}
#endif

The implementation can use a normal C++ class internally:

// counter_api.cpp
#include "counter_api.h"

class Counter {
public:
    explicit Counter(int initial) : value_(initial) {}

    int increment() {
        return ++value_;
    }

    int value() const {
        return value_;
    }

private:
    int value_;
};

extern "C" void* counter_create(int initial_value) {
    return new Counter(initial_value);
}

extern "C" int counter_increment(void* handle) {
    return static_cast<Counter*>(handle)->increment();
}

extern "C" int counter_value(const void* handle) {
    return static_cast<const Counter*>(handle)->value();
}

extern "C" void counter_destroy(void* handle) {
    delete static_cast<Counter*>(handle);
}

Go imports the C declarations in a cgo preamble and calls them through the C pseudo-package:

package main

/*
#cgo CXXFLAGS: -std=c++17
#cgo LDFLAGS: -lstdc++
#include "counter_api.h"
*/
import "C"

import (
    "fmt"
    "runtime"
)

type Counter struct {
    handle C.uintptr_t
}

func NewCounter(initial int) *Counter {
    ptr := C.counter_create(C.int(initial))
    if ptr == nil {
        return nil
    }

    c := &Counter{handle: C.uintptr_t(uintptr(ptr))}
    runtime.SetFinalizer(c, (*Counter).Close)
    return c
}

func (c *Counter) Increment() int {
    return int(C.counter_increment(unsafePointer(c.handle)))
}

func (c *Counter) Value() int {
    return int(C.counter_value(unsafeConstPointer(c.handle)))
}

func (c *Counter) Close() {
    if c.handle != 0 {
        C.counter_destroy(unsafePointer(c.handle))
        c.handle = 0
    }
}

func unsafePointer(h C.uintptr_t) unsafe.Pointer {
    return unsafe.Pointer(uintptr(h))
}

func unsafeConstPointer(h C.uintptr_t) unsafe.Pointer {
    return unsafe.Pointer(uintptr(h))
}

func main() {
    counter := NewCounter(10)
    defer counter.Close()

    fmt.Println(counter.Increment()) // 11
    fmt.Println(counter.Value())     // 11
}

The preceding Go example illustrates the wrapper shape, but it requires importing unsafe. In practice, it is simpler and more idiomatic to store the handle directly as unsafe.Pointer, as shown in the mini-project solution.

Key syntax rules:

  • import "C" must appear immediately after the cgo comment; no blank line may separate them.
  • Files containing C++ implementations should use the .cc, .cpp, or .cxx extension so Go invokes a C++ compiler.
  • Functions used by cgo should have C-compatible parameter and return types, such as integers, char*, and void*.
  • Put extern "C" around exported wrapper declarations and definitions.

Step by Step Execution

Consider this smaller call flow:

counter := NewCounter(5)
defer counter.Close()
next := counter.Increment()
  1. NewCounter(5) calls C.counter_create(C.int(5)).
  2. The C++ wrapper runs new Counter(5) and returns the object's address as void*.
  3. Go stores that address as an opaque handle. Go does not know the fields inside Counter.
  4. counter.Increment() passes the same handle to counter_increment.
  5. The wrapper converts void* back to Counter* with static_cast<Counter*>(handle).
  6. C++ calls Counter::increment(), increasing the internal value from 5 to 6.
  7. The wrapper returns 6, which cgo converts to C.int, and Go converts to int.
  8. At the end of the function, defer counter.Close() calls counter_destroy.
  9. counter_destroy uses delete to release the object allocated by new.

The important rule is symmetry: the side that creates a resource should provide the API that releases it. Here, C++ uses new, so C++ must use delete.

Real World Use Cases

This pattern is useful when Go needs functionality that already exists in a C++ library.

  • Image and video processing: wrap a C++ image decoder, encoder, or computer-vision library.
  • Database and storage engines: call a mature C++ embedded engine through a narrow API.
  • Hardware SDKs: use a vendor's C++ library for cameras, sensors, or industrial devices.
  • High-performance algorithms: reuse a tested C++ implementation of compression, geometry, or signal processing.
  • Legacy migration: build a new Go service while preserving a valuable C++ domain library.

For each case, keep the wrapper API small. Prefer functions that accept simple inputs and return simple results rather than exposing a large C++ object graph.

Take Quiz

Real Codebase Usage

In production code, developers usually avoid exposing every C++ method directly. Instead, they design a stable boundary API.

Use a Go type to own the native handle

type Engine struct {
    ptr unsafe.Pointer
}

The Go type groups the C++ handle with Go-friendly methods such as Start, Process, and Close.

Provide an explicit Close method

Native resources are not Go heap memory. Release them deterministically:

func (e *Engine) Close() {
    if e.ptr != nil {
        C.engine_destroy(e.ptr)
        e.ptr = nil
    }
}

Use defer engine.Close() immediately after a successful constructor call when appropriate. A finalizer can be a backup, but it should not be the primary cleanup mechanism because its timing is not predictable.

Validate before crossing the boundary

Validate ordinary Go inputs before calling C++:

if width <= 0 || height <= 0 {
    return errors.New("dimensions must be positive")
}

This reduces native calls and keeps predictable errors in Go.

Convert C++ failures into error codes or messages

Do not allow C++ exceptions to cross the C ABI boundary. Catch exceptions in the wrapper and return an error code, nullptr, or an error string that Go can interpret.

Keep Go pointers out of long-lived C++ storage

C++ must not retain a pointer to ordinary Go-managed memory after the cgo call returns. If native code needs to retain data, copy it into C/C++ allocated memory or use an approved mechanism such as runtime/cgo.Handle for passing Go values through C safely.

Common Mistakes

Trying to include a C++ class directly in cgo

This does not create a usable Go class binding:

/*
#include "Counter.hpp"
*/
import "C"

cgo is built around C declarations. Create extern "C" wrapper functions instead.

Forgetting extern "C"

Broken wrapper declaration:

void* counter_create(int initial_value);

Without C linkage, the C++ compiler may mangle the symbol name. cgo or the linker may then fail to find the expected function. Use:

extern "C" void* counter_create(int initial_value);

Letting C++ exceptions escape

Broken code:

extern "C" int parse_value(const char* text) {
    return std::stoi(text); // may throw
}

An exception crossing a C ABI boundary is unsafe. Catch it in C++ and return a status code or failure value.

Leaking native objects

Broken Go usage:

counter := NewCounter(0)
fmt.Println(counter.Increment())
// Close is never called.

Call Close, usually with defer, to release the C++ object.

Using an object after Close

After counter_destroy, the pointer is invalid. Set the Go handle to nil or zero and make methods reject closed objects.

Passing Go memory for C++ to keep

A C++ library must not store a pointer into Go memory for later use. Copy the data on the C++ side if it must outlive the call.

Assuming cgo makes all calls free

Crossing the Go/C++ boundary has overhead. Prefer batch operations, such as processing a whole buffer at once, over calling C++ once per byte or item.

Comparisons

ApproachBest forMain limitation
cgo with a C wrapper around C++Reusing a specific C++ library from GoRequires native builds and careful ownership rules
Direct C library through cgoLibraries that already expose a C APIStill has cgo boundary and build complexity
Reimplementing in GoPortability, simple deployment, idiomatic GoMay require substantial development effort
Separate C++ service using RPCStrong process isolation or independent deploymentAdds network/process communication overhead
C++ featureWhat to expose to Go instead
Class constructorcreate function returning void*
Instance methodFunction whose first argument is void* handle
Destructordestroy function
C++ exceptionError code, null handle, or allocated error message
std::stringconst char* plus length, or a dedicated buffer API
TemplateA concrete wrapper for each required type
Take Quiz

Cheat Sheet

  • Use cgo to call C-compatible functions from Go.
  • Wrap C++ APIs in functions declared with extern "C".
  • Compile implementation files as .cpp, .cc, or .cxx.
  • Represent C++ instances as opaque void* handles.
  • Typical wrapper API: create, operation functions, destroy.
  • Store the handle in a Go struct, usually as unsafe.Pointer.
  • Add an idempotent Close() method and call it explicitly.
  • Do not let C++ exceptions cross the wrapper boundary.
  • Do not let C++ retain ordinary Go pointers after a cgo call.
  • Prefer coarse-grained native calls to many tiny calls.
  • Put import "C" immediately after its cgo comment.
  • Use #cgo CXXFLAGS for C++ compiler options and #cgo LDFLAGS for linker libraries.

FAQ

Can Go call C++ directly?

Not in the same way it calls Go functions. Go uses cgo to call C-compatible functions, so C++ code normally needs an extern "C" wrapper.

Why is extern "C" needed when the code is compiled as C++?

It requests C linkage, avoiding C++ name mangling and making the exported functions callable through a C ABI.

Can Go use a C++ class as a Go struct?

No. Treat the C++ object as an opaque native handle and expose its operations as wrapper functions.

Should I use runtime.SetFinalizer to free a C++ object?

Use an explicit Close method as the primary mechanism. Finalizers are nondeterministic and should only be a backup when appropriate.

Can a C++ exception be caught in Go?

No. Catch it inside the C++ wrapper and translate it into a C-compatible error result.

Can I pass a Go slice to C++?

You can pass its data during a call under cgo's pointer rules, but C++ must not save that Go pointer for use after the call returns. Copy the data if it must be retained.

Does cgo work with cross-compilation?

It can, but the target platform needs an appropriate C/C++ cross-compiler and compatible native libraries. This is more involved than pure-Go cross-compilation.

Take Quiz

Mini Project

Description

Build a small Go wrapper around a C++ Counter class. The project demonstrates the standard create/use/destroy lifecycle for a native C++ object and exposes it through an idiomatic Go type.

Goal

Create, increment, read, and explicitly release a C++ counter from a Go program.

Requirements

Requirement list is provided below.

Take Quiz

Keep learning

Related questions

View more