Skip to content

[Go] Add Go bindings for ONNX Runtime C API - #29615

Merged
Tianlei Wu (tianleiwu) merged 10 commits into
microsoft:mainfrom
dannyota:dannyota/go-bindings
Aug 13, 2026
Merged

[Go] Add Go bindings for ONNX Runtime C API#29615
Tianlei Wu (tianleiwu) merged 10 commits into
microsoft:mainfrom
dannyota:dannyota/go-bindings

Conversation

@dannyota

@dannyota Danny On The Air (dannyota) commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds official Go bindings for the ONNX Runtime C API using CGO, following
the structure of the existing language bindings.

Closes #9786.

Motivation

Go is widely used for backend services that perform ML inference, including
embeddings, classification, and NLP workloads. This PR provides a supported
Go interface without requiring third-party runtime dependencies.

Highlights

  • Sessions: create sessions from files or byte buffers and inspect
    inputs and outputs, including dynamic dimensions
  • Tensor I/O: generic numeric tensors, byte-backed tensors, string
    tensors, scalar tensors, and zero-length dimensions
  • Session options: thread counts, graph optimization, execution mode,
    memory arena, profiling, and free-dimension overrides
  • Execution Providers: CUDA V2, TensorRT V2, and a generic key-value
    configuration API for other EPs
  • I/O binding: bind inputs and outputs to devices for accelerator
    workflows
  • Metadata and value inspection: model metadata, sequences, and maps
  • Run options: logging, tags, configuration entries, and cancellation
    through context.Context
  • Concurrency: concurrent Session.Run calls supported by the wrapper

Design

The bindings access the ONNX Runtime C API vtable through C shim functions
because CGO cannot directly call C function pointers.

The ONNX Runtime shared library is loaded at runtime with dlopen on
Linux/macOS and LoadDLL on Windows. A single OrtEnv is shared within
the process.

Numeric input tensors use runtime.Pinner to avoid copying their backing
storage. Output tensors expose ORT-allocated memory that remains valid until
the value is closed.

Filesystem paths use ORTCHAR_T: UTF-16 on Windows and UTF-8 elsewhere.
Paths containing NUL bytes are rejected instead of being silently truncated.

  • Module: github.com/microsoft/onnxruntime/go
  • Package: onnxruntime
  • Minimum Go version: 1.26
  • Dependencies: Go standard library and CGO only

Correctness and platform hardening

The implementation includes safeguards for several lifecycle and C-boundary
conditions:

  • Cancellation watchers are stopped before their run options are released,
    preventing stale cancellation from affecting later inference calls.
  • Cancellation works with both internally created and caller-provided
    RunOptions.
  • I/O binding keeps its associated session alive and locked across C calls.
  • Nil and closed values are rejected before their handles reach the C API.
  • Windows model paths are passed to ORT as ORTCHAR_T.
  • Tensor element-count and byte-size calculations are checked for overflow.
  • Library handles are released when initialization fails.
  • Initialization and shutdown state is synchronized.
  • Negative intra-op and inter-op thread counts are rejected, with boundary
    tests for -1, 0, and 1.

Linux CI coverage

The existing Linux x64 Release job now tests the Go bindings against the
libonnxruntime.so built from the same checkout.

The job:

  • Verifies that the shared library exists
  • Loads that exact library through ORT_LIB_PATH
  • Runs go test -race -count=1 ./onnxruntime
  • Fails if the library is missing, cannot be loaded, or the tests fail

-count=1 prevents a cached Go test result from hiding a problem with the
newly built library.

Tests

Local validation covers:

  • Session lifecycle and inference correctness
  • Dynamic shapes and zero-length dimensions
  • Numeric, Boolean, string, byte-backed, and scalar tensors
  • Type and shape mismatches
  • Nil, closed, and use-after-close handling
  • Concurrent inference on a shared session
  • Context cancellation and run options
  • I/O binding and model metadata
  • Non-ASCII model paths and NUL-path rejection
  • Sequence and map values
  • Qwen3-Embedding-0.6B ONNX INT8 integration

The complete binding suite passes under the race detector against:

  • ONNX Runtime 1.29 built locally with GCC 14
  • ONNX Runtime 1.27 for compatibility coverage

Additional validation completed:

  • GOEXPERIMENT=cgocheck2
  • go vet
  • golangci-lint
  • repository lintrunner
  • actionlint
  • MinGW Windows cross-build with incompatible-pointer checks enabled
  • Regenerated test models verified byte-for-byte unchanged

BenchmarkSessionRun was added to track successful inference allocations.
Avoiding eager validation-message formatting reduced the local benchmark
from 504 B and 17 allocations to 440 B and 13 allocations per run.

Idiomatic Go wrapper for the ORT C API via CGO, matching the pattern
of existing language bindings (rust/, java/, csharp/).

Covers session management, typed/untyped tensor I/O, string tensors,
IO binding, model metadata, run options, session options with CUDA and
TensorRT execution providers, context cancellation, and concurrent
inference. Tested with real Qwen3-Embedding-0.6B ONNX INT8 model.
@dannyota

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

Fix TOCTOU race in NewIOBinding (use-after-free on concurrent Close),
empty outputNames panic in Run/RunWithOptions, and metadata use-after-
close segfault. Add 15 tests covering all identified coverage gaps.
Remove dead code. Bump minimum ORT version to 1.27.0 for getter APIs.
@dannyota

Copy link
Copy Markdown
Contributor Author

Updated with a second commit:

  • Fixed TOCTOU race in NewIOBinding (use-after-free on concurrent Close)
  • Fixed empty outputNames panic in Run/RunWithOptions
  • Fixed metadata use-after-close segfault
  • Added 15 tests (63 total), covering all tensor types, error paths, IO binding, and edge cases
  • Bumped minimum ORT to 1.27.0 for GetMemPatternEnabled/GetSessionExecutionMode getters
  • Tested against ORT 1.28.0 built from rel-1.28.0 branch — all passing

Try API version 27 first, fall back to 17 for ORT 1.17-1.26.
GetExecutionMode and IsMemPatternEnabled return a clear error
on older libraries instead of crashing. Minimum ORT is now 1.17.
@dannyota

Copy link
Copy Markdown
Contributor Author

Added API version fallback: bindings now try API version 27 first, fall back to 17 for older ORT libraries. This means ORT 1.17+ is supported (previously required 1.27+). The two getter functions added in 1.27 (GetExecutionMode, IsMemPatternEnabled) return a clear error on older libraries instead of crashing.

Add GetVersion/APIVersion, cache CString names in Session for zero-
alloc Run hot path, enable/disable telemetry, SetOptimizedModelFilePath,
RegisterCustomOpsLibrary, Has/GetSessionConfigEntry, NewSequence,
NewMap, NewMapFromGoMap. Fix API version probing to try 27 down to 17
for broader ORT compatibility. Fix IsSequence/IsMap enum values.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an official Go (cgo) binding layer for the ONNX Runtime C API, including runtime library loading, core session/tensor APIs, and a fairly comprehensive Go test suite with small ONNX model fixtures.

Changes:

  • Added Go package go/onnxruntime with wrappers for environment lifecycle, sessions, tensors (typed + raw-bytes + string), session/run options, IO binding, and model metadata.
  • Added a C shim layer (cshim.h/cshim.c) to call through the OrtApi vtable (since cgo can’t call function pointers directly).
  • Added test models and Go unit/integration tests covering common workflows (including dynamic shapes, zero-length dims, concurrency, and optional Qwen3 integration).

Reviewed changes

Copilot reviewed 29 out of 34 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
go/testdata/gen_models.py Generates minimal ONNX models used by the Go tests.
go/onnxruntime/types.go Defines Go enums/types for tensor dtypes, graph optimization level, execution mode, and generic tensor element constraints.
go/onnxruntime/tensor.go Implements typed tensors, raw-byte tensors, sequence/map wrappers, and tensor shape/type helpers.
go/onnxruntime/tensor_test.go Unit tests for tensor creation, accessors, and sequence/map helpers.
go/onnxruntime/string_tensor.go Implements string tensor creation and string extraction.
go/onnxruntime/string_tensor_test.go Unit tests for string tensor creation and reading.
go/onnxruntime/session.go Implements session creation, IO introspection, Run/RunWithOptions, profiling end, and lifecycle management.
go/onnxruntime/session_test.go Session tests covering model IO, inference, dynamic/zero-len dims, concurrency, cancellation, and options.
go/onnxruntime/run_options.go Run options wrapper (verbosity, severity, tag, terminate, config entries).
go/onnxruntime/run_options_test.go Tests for run options and RunWithOptions behavior.
go/onnxruntime/qwen3_test.go Optional integration test for a locally available Qwen3 embedding model.
go/onnxruntime/ort.go Global initialization/shutdown, telemetry toggles, provider enumeration, and library path resolution.
go/onnxruntime/ort_test.go Package TestMain + tests for init/idempotency, providers, telemetry, shutdown constraints, and OrtError typing.
go/onnxruntime/options.go SessionOptions wrapper including EP configuration, profiling, free-dim overrides, getters (API-gated), and misc config.
go/onnxruntime/options_test.go Tests for SessionOptions cloning, memory settings, execution mode, getters, profiling, EP errors, and config entries.
go/onnxruntime/onnxruntime_error_code.h Error-code header copy for the shim/include surface.
go/onnxruntime/metadata.go Model metadata wrapper (producer, graph, domain, description, version, custom KV).
go/onnxruntime/metadata_test.go Tests for metadata access, double-close, and use-after-close behavior.
go/onnxruntime/load_windows.go Windows dynamic loading via LoadDLL + FindProc.
go/onnxruntime/load_unix.go Unix dynamic loading via dlopen/dlsym.
go/onnxruntime/iobinding.go IO binding API (bind inputs/outputs, run, fetch bound outputs/names).
go/onnxruntime/iobinding_test.go IO binding tests (basic run, output binding modes, output names/values, memory info, closed session).
go/onnxruntime/errors.go Go error mapping for OrtStatus + wrapping helpers.
go/onnxruntime/doc.go Package-level documentation for initialization and concurrency expectations.
go/onnxruntime/cshim.h Declares the C shim surface used by cgo.
go/onnxruntime/cshim.c Implements shim functions that forward to OrtApi function pointers.
go/go.mod Declares Go module path and minimum Go version.
go/.golangci.yml Lint configuration for the Go code.
go/.gitignore Ignores local .ort-lib/ test library directory.

Comment thread go/onnxruntime/ort.go
Comment thread go/onnxruntime/tensor.go
Comment thread go/onnxruntime/tensor.go
Comment thread go/onnxruntime/tensor.go
Comment thread go/onnxruntime/string_tensor.go Outdated
Comment thread go/onnxruntime/iobinding.go
Comment thread go/onnxruntime/load_unix.go
Comment thread go/onnxruntime/load_unix.go
Comment thread go/onnxruntime/load_windows.go
Comment thread go/onnxruntime/tensor.go
Address the PR review findings plus a follow-up audit of the same defect
classes elsewhere in the package.

Crashes and memory safety:
- runInner released the OrtRunOptions before joining the cancellation
  watcher. Defers run LIFO, so the freed block could be handed to a
  concurrent Run whose live options the stale watcher then terminated,
  aborting an unrelated inference. Release now happens after the join.
- IOBinding methods checked only the binding handle, never whether the
  Session had been closed, passing a NULL OrtSession into ORT. Binding
  calls now hold the session read lock across the C call.
- Run, Bind*, NewSequence, NewMap and AddInitializer accepted nil or
  closed tensors and passed a NULL OrtValue into ORT. Validation is
  centralized in Tensor.checkUsable.
- NewIOBinding(nil) panicked instead of returning an error.
- StringData passed a pointer to a Go slice header into C when a tensor
  held only empty strings, violating the cgo pointer-passing rules.
- Guard element-count and byte-size overflow before slicing ORT memory.
  Bytes now errors on string tensors instead of returning an empty slice.

Context cancellation:
- RunWithOptions ignored cancellation whenever the caller supplied
  RunOptions, so deadlines were a silent no-op. The terminate watcher now
  runs for any cancellable context; caller-owned options are restored
  afterwards because the terminate flag is sticky.

Resource leaks:
- loadLibrary discarded the module handle, so every Init failure after a
  successful load leaked the mapping. Init now closes it on all failure
  paths, and IO binding allocations are freed only when non-nil.

Windows:
- The C shim declared path parameters as char*, but ORT uses ORTCHAR_T,
  which is wchar_t on Windows. Model paths were therefore passed as
  narrow strings and opened as mojibake. Shim signatures now use
  ORTCHAR_T and paths are converted per platform. Paths containing NUL
  are rejected rather than silently truncated.

Concurrency:
- initialized and shutdown are now atomic; checkInit no longer races
  with Init.

Verified with go vet, golangci-lint and Semgrep, the race detector,
cgocheck2, and a mingw Windows cross-build. 102 tests pass, 3 skip.
@dannyota

Copy link
Copy Markdown
Contributor Author

All 13 review comments are addressed in 10fe857.

A follow-up audit of the same defect classes turned up several more serious issues, fixed in the same commit:

  • Use-after-free on the cancellation path. runInner released the OrtRunOptions before joining the terminate watcher — defers run LIFO. The freed block could be handed straight to a concurrent Run, whose live options the stale watcher then terminated, aborting an unrelated, uncancelled inference. Reproduced under -race with MALLOC_PERTURB_.
  • RunWithOptions ignored context cancellation whenever the caller supplied RunOptions, so deadlines were a silent no-op and the call blocked in C for the full inference. The terminate watcher now runs for any cancellable context, and caller-owned options are restored afterwards because the terminate flag is sticky.
  • IOBinding never checked whether its Session had been closed, passing a NULL OrtSession into ORT — a SIGSEGV, not a recoverable panic. Binding calls now hold the session read lock across the C call rather than checking and then racing.
  • Four further instances of the nil/closed-tensor class the review flagged: NewSequence, NewMap, AddInitializer, and NewIOBinding(nil). Validation is centralized in one helper.
  • The library-handle leak was broader than reported. loadLibrary discarded the module handle entirely, so all four Init failure paths after a successful load leaked the mapping, not just the dlsym path.

Windows path handling — please review, this changes the shim's C signatures

The shim declared path parameters as char*, but ORT uses ORTCHAR_T, which is wchar_t on Windows. Model paths were therefore passed as narrow strings and opened as mojibake. The four path-taking shim functions now take ORTCHAR_T, with per-platform conversion on the Go side.

Verified with a mingw cross-build under -Werror=incompatible-pointer-types. Reverting the shim to char* makes that build fail on all four functions, so the typing is load-bearing — and GCC 14+ would turn the existing mismatch into a hard build error regardless. Paths containing NUL are now rejected rather than silently truncated (previously the path was cut at the NUL and a different model loaded).

A note on three of the comments

The two "don't free a nullptr through the allocator" findings and the TensorData element-count check are defensive rather than live bugs: this code only uses ORT's default allocator, whose Free handles NULL, and a tensor with more elements than addressable memory could never have been allocated in the first place. The guards are in anyway since they cost nothing — flagging it so the checks don't read as unexplained.

Verification

102 tests pass, 3 skip (two need ORT ≥ 1.27; one is a subprocess race child). Clean under -race, cgocheck2, golangci-lint, and Semgrep. Every new test has a negative control confirming it fails when its fix is reverted — three tests from the first pass turned out to pin nothing and were replaced.

Danny On The Air (dannyota) added a commit to dannyota/dannyota that referenced this pull request Jul 17, 2026
Link to microsoft/onnxruntime#29615 — Danny's open PR adding
official Go bindings for ONNX Runtime C API via CGO.
Comment thread go/testdata/gen_models.py Fixed
Comment thread go/onnxruntime/cshim.h Fixed
Comment thread go/testdata/gen_models.py Fixed
Comment thread go/testdata/gen_models.py Fixed
Comment thread go/testdata/gen_models.py Fixed
Comment thread go/testdata/gen_models.py Fixed
Comment thread go/testdata/gen_models.py Fixed
Comment thread go/testdata/gen_models.py Fixed
Comment thread go/testdata/gen_models.py Fixed
Comment thread go/testdata/gen_models.py Fixed
Comment thread go/testdata/gen_models.py Fixed

@tianleiwu Tianlei Wu (tianleiwu) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The C-boundary ownership, cancellation, and teardown hardening are well done, and the local regression suite is broad. Before merging an official binding, please add a required CI path that actually loads ONNX Runtime; currently the suite can execute zero tests and still report success. I also left two non-blocking suggestions on configuration validation and inference-path allocations.

Comment thread go/onnxruntime/ort_test.go
Comment thread go/onnxruntime/options.go Outdated
Comment thread go/onnxruntime/session.go
Run race tests against the shared library produced by the Linux x64 Release build.

Reject negative thread counts and benchmark Session.Run allocations while avoiding eager error formatting.
Apply repository formatters and use consistent ONNX imports and lowercase locals without changing generated model contents.
@dannyota

Copy link
Copy Markdown
Contributor Author

Tianlei Wu (@tianleiwu) Thank you for the thoughtful review. I’ve addressed the three comments and re-requested your review. The new workflow runs are currently waiting for maintainer approval. When you have a chance, could you please approve the runs and take another look? Thanks!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tianlei Wu (@tianleiwu) All workflow runs have been approved and all 86 checks are green. Ready for another look when you have a chance.

@tianleiwu

Copy link
Copy Markdown
Contributor

Danny On The Air (@dannyota), can we move *_test.go to a separated folder?

@tianleiwu Tianlei Wu (tianleiwu) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at f2f62ab. The previous requested changes are addressed: Linux x64 Release now runs the race-enabled Go suite against the shared library built from the same checkout, negative thread counts are rejected with boundary coverage, and the inference allocation cost is recorded after deferring error formatting to failure paths.

I also traced the latest sequence-buffer pinning through the native CreateValue and GetValue implementations; it matches their shallow-copy and deep-copy ownership behavior. No additional blocking findings remain.

Local exact-head vet, formatting, and diff checks passed. The required Go runtime CI job is still running; the current macOS failure is an Abseil download error, and the arm64 job terminated mid-build without a source diagnostic.

Picks up the ContribOperators.md regeneration (microsoft#31985), which fixes the
Windows GPU Kernel Documentation Validation failure. That check was
failing on main independently of this branch.

The Go binding CI wiring in linux_ci.yml and reusable_linux_build.yml
auto-merged cleanly and is unchanged.
@dannyota

Copy link
Copy Markdown
Contributor Author

Happy to move them if you'd prefer — though my instinct is to keep them in place.

Go's convention is *_test.go beside the package source, and each binding here already follows its own language's layout (java/src/test/java for Maven, rust/onnxruntime/tests/ for Cargo, csharp/test as a separate assembly).

One catch either way: a few tests reach into unexported internals (loadLibrary/closeLibrary, Session.runInner, shapeElementCount), so they'd have to stay behind and the suite would end up split across two directories.

If the aim is exercising the public API the way a user would, there's a cheaper middle ground — the same files can declare package onnxruntime_test in place, which limits them to exported identifiers. 7 of the 10 already compile that way.

Either works for me, just let me know which you'd like.


Separately, I've merged current main into the branch to pick up the ContribOperators.md regeneration (#31985), which should clear the Windows GPU Kernel Documentation Validation failure.

@tianleiwu
Tianlei Wu (tianleiwu) enabled auto-merge (squash) August 13, 2026 09:13
@tianleiwu
Tianlei Wu (tianleiwu) merged commit 72e1c9c into microsoft:main Aug 13, 2026
86 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Do we have official Golang support for ONNXRuntime?

4 participants