Skip to content

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

Open
dannyota wants to merge 5 commits into
microsoft:mainfrom
dannyota:dannyota/go-bindings
Open

[Go] Add Go bindings for ONNX Runtime C API#29615
dannyota wants to merge 5 commits into
microsoft:mainfrom
dannyota:dannyota/go-bindings

Conversation

@dannyota

@dannyota dannyota commented Jul 8, 2026

Copy link
Copy Markdown

Description

Official Go bindings for the ONNX Runtime C API via CGO, following
the pattern of existing language bindings (rust/, java/, csharp/).

Closes #9786

Motivation

Go is widely used for backend services that need ML inference
(embeddings, classification, NLP). There is strong community demand
(#9786, 19 comments) and no official Go support. This PR fills that gap.

What's included

  • Session management: create from file or byte buffer, introspect
    inputs/outputs with dynamic dimension support
  • Tensor I/O: generic CreateTensor[T] (Go 1.18+ generics),
    untyped NewTensorFromBytes, string tensors, zero-length dimensions
  • Session options: thread counts, graph optimization, execution mode,
    memory arena, profiling, free dimension overrides
  • Execution providers: CUDA (V2), TensorRT (V2), generic string
    key-value API for all other EPs
  • IO Binding: bind inputs/outputs to devices for GPU workflows
  • Model metadata: producer, graph name, domain, version, custom keys
  • Run options: log level, tag, config entries
  • Cancellation: context.Context support via RunOptionsSetTerminate,
    honored by both Run and RunWithOptions
  • Concurrency: Session.Run safe for concurrent use (ORT guarantee)
  • Sequence/Map: value type inspection and sequence element access

Design

Wraps the C API vtable (OrtApi) through ~80 C shim functions in
cshim.h/cshim.c — CGO cannot call C function pointers directly.
Library loaded at runtime via dlopen (Linux/macOS) or LoadDLL
(Windows). Singleton OrtEnv per process via sync.Mutex.

Zero-copy tensor inputs via runtime.Pinner. Output tensors wrap
ORT-allocated memory with views valid until Close().

Filesystem paths are passed as ORTCHAR_T, so they are UTF-16 on
Windows and UTF-8 elsewhere; conversion is per-platform on the Go side
(path_windows.go / path_unix.go). Paths containing NUL are rejected
rather than silently truncated.

Module path: github.com/microsoft/onnxruntime/go
Package: onnxruntime (import as ort)
Go 1.26 minimum. stdlib + CGO only — no third-party dependencies.

Correctness and platform hardening

The automated review and a follow-up audit of the same defect classes
produced the fixes in 10fe857. The substantive ones:

  • Use-after-free on the cancellation path: run options were released
    before the terminate watcher was joined, so a stale watcher could
    terminate a concurrent, uncancelled inference.
  • Cancellation was a silent no-op whenever the caller supplied their
    own RunOptions.
  • IO binding calls on a closed Session passed a NULL OrtSession
    into ORT. Binding calls now hold the session read lock across the C call.
  • nil/closed tensors were accepted by Run, Bind*, NewSequence,
    NewMap and AddInitializer, passing NULL OrtValue into ORT.
  • Windows model paths were passed as narrow strings: the shim declared
    path parameters as char* where ORT expects ORTCHAR_T (wchar_t on
    Windows), so paths were opened as mojibake. The four path-taking shim
    functions now take ORTCHAR_T. Note this changes those shim signatures.
  • Element-count and byte-size overflow guards before slicing ORT memory;
    the library handle is no longer leaked on Init failure paths;
    initialized/shutdown are atomic.

Tests

102 tests passing, 3 skipped (two require ORT ≥ 1.27; one is a subprocess
race child):

  • Lifecycle, inference correctness, dynamic shapes, zero-length dims
  • All tensor types (float32, int64, bool, string, bytes, scalars)
  • Error handling, type/shape mismatch, use-after-close
  • Concurrency: 16 goroutines × 50 runs on one session
  • Context cancellation, IO binding, model metadata, run options
  • Non-ASCII model paths; NUL-in-path rejection
  • Integration: Qwen3-Embedding-0.6B ONNX INT8 (59 inputs, 57 outputs)

Verified clean under the race detector, GOEXPERIMENT=cgocheck2, and a
mingw Windows cross-build with -Werror=incompatible-pointer-types.
Each regression test carries a negative control confirming it fails when
its fix is reverted.

  • Semgrep Pro: 0 findings
  • golangci-lint: 0 issues

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

dannyota commented Jul 8, 2026

Copy link
Copy Markdown
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

dannyota commented Jul 8, 2026

Copy link
Copy Markdown
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

dannyota commented Jul 8, 2026

Copy link
Copy Markdown
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
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.

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?

2 participants