Skip to content
LoyieKingPublic

About

goc: a C dialect that compiles onto Go goroutine stacks (morestack / stackmap / write barriers), aiming to embed QuickJS without the cgo tax.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

10 stars

Watchers

0 watching

Forks

Repository files navigation

GOC

English | 简体中文

A C dialect that lets Go call C libraries efficiently.

license

C code compiled by goc runs directly on the goroutine stack. Go calls it like an ordinary Go function, with no cgo in between. goc is experimental and currently supports linux/amd64 only.

Background

The core reason Go and C cannot interoperate efficiently is Go's growable stack.

Go's growable stacks

To support huge numbers of goroutines, Go gives each goroutine a very small initial stack (a few KB). Yet Go programs almost never overflow the stack, because every Go function checks on entry whether there is enough stack left. If not, the runtime allocates a larger stack, copies the old stack over, and carries on.

Because the stack can move, any pointer into the stack becomes a dangling pointer after the move. Go handles this in a few cases:

  • Pointers on the stack that point into the stack. The compiler records which stack slots of each function hold pointers. When the stack moves, the runtime uses that record to rewrite every pointer into the old stack so it points into the new one. This requires every stack frame to have a size fixed at compile time, which is why Go has no variable-length arrays on the stack.

  • Pointers on the heap that point into the stack. When the stack moves, the runtime only fixes pointers on the stack, not on the heap. So Go must guarantee that the heap never points into a stack. It does this with escape analysis: if the address of a variable might be stored on the heap or in a global, the compiler simply allocates that variable on the heap. For example:

    var sink *int
    
    func f() {
        x := 42
        sink = &x // the compiler puts x on the heap, not on the stack
    }
  • Go calling non-Go code. C code does not know the stack can move, does not check for stack space on entry, and the compiler has no pointer records for it. So C cannot run directly on a goroutine stack. Go instead switches to a large stack that never moves for every call. This is safe, but every call pays a fixed cost.

Existing approaches

  • cgo (official): every C call switches to the system thread's large stack and notifies the scheduler. Safe and general, but each call costs tens of nanoseconds, which becomes a bottleneck for C functions that are called often and return quickly.
  • directcgo (experimental community project): first grows the goroutine stack to a large size (64 KB), then calls C directly on the goroutine stack without switching. Calls drop to a few nanoseconds, but every goroutine holds a large chunk of stack memory, and a C function that uses more stack than reserved can corrupt memory. It only suits short functions whose stack usage is predictable.
  • syscall and friends (e.g. purego): call prebuilt shared libraries without a C compiler. Under the hood they also switch to the system stack, so performance is in the same range as cgo. Code that instead runs foreign functions directly on the goroutine stack can hit stack overflows.

All of these work well for using C libraries that are already compiled. But if we can recompile the C library, we can go a step further: teach the C code itself about Go's stack.

Our approach

A new C dialect, goc, which brings Go's growable-stack rules to C, so Go can call goc code directly without any stack risk.

Every function goc compiles behaves like a Go function: it checks for stack space on entry, the compiler records the pointers it keeps on the stack, and those pointers are fixed up correctly when the stack moves. So it can run directly on the goroutine stack, and Go does not need to switch stacks to call it.

Stack pointers

This is the heart of the design. C code is full of pointers, and when Go moves a stack it only fixes the pointers it knows about. For C to run safely on a movable stack, the compiler must know where every pointer can point and where it can be stored.

goc therefore gives C pointers a "color":

Color Meaning
cptr Points to memory outside the stack (C heap, globals). Can be stored anywhere.
sptr Points to an object on the stack. Lives only on the stack; fixed up by the runtime when the stack moves.
uptr May or may not point into the stack. Can be stored anywhere; stack addresses are stored as relative positions, so they stay valid after a move.
gptr Points to a Go heap object and follows Go's garbage-collection rules.

Colors cannot be converted into each other freely. A plain T * is inferred to the right color from how the pointer is used, so existing C code such as QuickJS needs almost no rewriting. For example, storing a stack pointer into a global is allowed; the compiler turns that store into a uptr encoding automatically. Uses that cannot be fixed, such as returning a pointer to a local variable, are compile errors:

#include "goc.h"

static int *g_slot;

void keep(sptr(int) p) {
  g_slot = p;       /* OK: stored as uptr, restored on load */
}

int *bad_return(void) {
  int local = 1;
  return &local;    /* compile error: cannot return a stack pointer */
}

The full rules are in docs/syntax-guide.md (Chinese).

Compilation

The goc compiler is a modified Clang/LLVM: the frontend checks and infers pointer colors, the backend emits machine code that follows Go's stack rules, and the output is an object file the Go toolchain links directly. goc therefore interoperates seamlessly with Go while the C code still gets LLVM's full optimizer, keeping production-grade C performance: on the V8 benchmark, goc-ng reaches about 92% of native QuickJS-ng and about 4.1× Goja, and goc-bellard reaches about 90% of the gcc Bellard build and about 4.7× Goja (see Benchmarks).

Benchmarks

QuickJS is the showcase: goc compiles both QuickJS-ng and Fabrice Bellard's original QuickJS. The five JS engines below were measured on the same machine in the same session (2026-10-09, AMD Ryzen 9 7900X):

Engine Description
goc-ng QuickJS-ng compiled with goc
goc-bellard Bellard's QuickJS compiled with goc
native ng QuickJS-ng, native build (clang)
native Bellard Bellard's QuickJS, native build (gcc)
Goja JS engine written in pure Go

Speed relative to native ng

Suite goc-ng goc-bellard native ng native Bellard Goja How to read
V8-v7 total 1534 1781 1662 1988 377 score, higher is faster
SunSpider geomean (ms) 11.18 9.39 10.99 8.24 61.49 time, lower is faster
microbench geomean (ns) 46.7 35.5 42.9 31.4 152.1 time, lower is faster
Call microbenchmark (calls/ms) 27907 27772 29096 31277 6828 score, higher is faster
test262 sample passed 1502/1526 1501/1526 1502/1526 1501/1526 1453/1526
QuickJS official tests 73/77 77/77 73/77 77/77 58/77

Correctness: both goc builds match their native counterparts test for test.

Correctness

V8-v7 sub-tests:

V8-v7

Test goc-ng goc-bellard native ng native Bellard Goja
Richards 1034 1124 1088 1349 364
DeltaBlue 1022 1083 1096 1268 374
Crypto 987 1284 1049 1420 173
RayTrace 2423 2627 2776 2948 385
EarleyBoyer 2922 3059 3213 3574 653
RegExp 493 622 548 659 322
Splay 4307 4860 4889 5528 789
NavierStokes 1962 2646 1959 2702 267
Total 1534 1781 1662 1988 377

SunSpider and microbench:

SunSpider

microbench

Call microbenchmark:

microcall

Memory (peak RSS in MiB; the last row is the extra memory per additional JS runtime, in KiB):

Peak RSS

Workload goc-ng goc-bellard native ng native Bellard Goja
Empty script 8.2 8.1 2.8 2.8 5.6
Full V8-v7 164.4 155.8 155.6 146.9 1470.7
SunSpider (max) 14.6 15.4 8.0 7.4 23.0
microbench 10.7 10.5 4.5 4.3 567.3
Per extra runtime (KiB) 224 199 209 189 105

Multiple instances

goc builds carry a fixed overhead of about 5.3 to 5.4 MiB over native when idle. On the full V8 suite the ratio falls to 1.06.

Where goc is slower than native, the cost comes mainly from the extra work needed to follow Go's stack rules: stack checks, pointer encoding and decoding, and pointer records on the stack. Against a clang -O2 build of the same Bellard source, goc-bellard reaches 95% of that build on V8, and is 2% slower on SunSpider and 8% slower on microbench. The 2026-09-27 mechanism study is in docs/perf-gap.md. Current scores, charts, and methodology are in docs/benchmark.md (both in Chinese).

Architecture

flowchart LR
    A["C source"] --> B["Modified Clang frontend<br/>checks and infers pointer colors"]
    B --> C["LLVM optimizer"]
    C --> D["goc backend<br/>stack checks, pointer records"]
    D --> E["Go object file"]
    E --> F["Go linker<br/>links with Go code into one program"]
Loading
Directory Purpose
cmd/goc Command-line entry point
include/goc.h How to write pointer colors
clang/ Changes to the Clang frontend
llvm/ goc's LLVM passes (color/escape transform, stack-map plugin) and goc-llc
goobj/ Go-side libraries: vendored Go object writer, Go ABI lowering
tools/ goc pipeline scripts, gocobj2goobj (.gocobj → Go object), goc-lower
runtime/ goc runtime support
test/ Tests, including QuickJS
bench/ Benchmarks and perf-gap studies
scripts/ Build, release and test scripts
docs/ Documentation

More detail in ARCHITECTURE.md.

Usage

Requirements: linux/amd64 host, Go 1.18–1.27 (arm64 targets: Go 1.21+; see docs/multi-go-version.md). The release archive already contains the patched Clang. Building from a checkout with a Go older than 1.23 also needs a Go 1.23+ for the repository tools (GOC_HOST_GO).

Download and the smallest program: docs/quickstart.md.

tar -xzf goc-0.0.2-linux-amd64.tar.gz
cd goc-0.0.2-linux-amd64
export PATH="$PWD/bin:$PATH"
goc check
goc go examples/hello -o /tmp/hello
/tmp/hello

Building that archive from this repository is docs/build-from-source.md:

git clone https://github.com/LoyieKing/goc.git
cd goc
./scripts/build-clang.sh
./scripts/build-passes.sh
./cmd/goc check

Call goc from Go. The smallest program is examples/hello. The golden test is test/e2e/goabi:

./test/e2e/goabi/run.sh

Write a normal function in C and a body-less declaration in Go, and Go can call it directly:

// goabi.c
int goabi_add2(int a, int b) { return a + b; }
// main.go
func goabi_add2(a, b int32) int32

Build QuickJS:

git clone https://github.com/quickjs-ng/quickjs.git third_party/quickjs-ng
./scripts/qjs-cli-build.sh       # produces build/qjs/qjscli

For Bellard's QuickJS, put it in third_party/quickjs-bellard and run QJS_FLAVOR=bellard ./scripts/qjs-cli-build.sh. Benchmark scripts and methodology are in docs/benchmark.md; step-by-step details and common pitfalls are in docs/guide.md (Chinese and English).

Current limitations:

  • linux/amd64 only.
  • Functions Go calls in goc may only use basic types, pointers and small structs in their signatures.
  • A stack pointer may only be used on the goroutine that created it.
  • goc calling Go still needs hand-written call code.

Roadmap

See docs/todo.md (Chinese):

  • An out-of-the-box goc compiler: the release archive is goc plus the patched Clang (docs/quickstart.md). goc go and goc check take flags. There is no clang path to pass.
  • Measure the cost of calls between goc and Go: per-call cost of Go calling goc and goc calling Go, compared with Go calling Go and cgo.
  • QuickJS using Go's GC directly: allocate JS objects on the Go heap and let the Go garbage collector reclaim them.
  • linux/arm64 support.
  • goc calling Go: call Go with just a declaration, the same way Go calls goc.
  • Stack pointers across goroutines.
  • Pointer-color visualization and editor support: see in your editor which color each pointer was inferred to.
  • Keep closing the performance gap with native C.

Documentation

Document
docs/quickstart.md Download the release and run goc
docs/build-from-source.md Build the release archive from a checkout
docs/guide.md User guide (Chinese / English)
docs/toolchain.md goc go flags and where clang lives
docs/syntax-guide.md Language rules (Chinese)
ARCHITECTURE.md Pipeline, directory layout, passes and tools
docs/runtime-model.md uptr encoding, alloca, what is not supported
docs/benchmark.md Benchmarks (Chinese)
docs/perf-gap.md Performance gap analysis (Chinese)
docs/todo.md Roadmap (Chinese)
CONTRIBUTING.md Contributing

License

MIT — Copyright (c) 2026 Loyie King. See LICENSE and NOTICE.

About

goc: a C dialect that compiles onto Go goroutine stacks (morestack / stackmap / write barriers), aiming to embed QuickJS without the cgo tax.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

10 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages