Skip to content

proposal: generate LLAR bindings with llgo get #2167

Description

@MeteorsLiu

Proposal: Generate LLAR Bindings with llgo get

Status: Draft

1. Summary

llgo get should use LLAR to install a requested native module and use llcppg to generate its LLGo binding locally.

The core pipeline is:

flowchart LR
    A["llgo get madler/zlib@v1.2.1"]
    B["llar install madler/zlib@v1.2.1 --json"]
    C["Read module, dependencies, and pkg-config metadata"]
    D["Generate dependency bindings"]
    E["Run llcppg for madler/zlib"]
    F["LLGoCacheDir/madler/zlib@v1.2.1"]

    A --> B --> C --> D --> E --> F
Loading

The selected LLAR native-module version is also the cache version of the generated binding. LLGo does not publish or maintain a second, independently assigned wrapper version.

2. Background

The previous llpkg design stored pre-generated wrappers in a dedicated project and maintained a mapping between native-library versions and Go wrapper versions.

That design also acted as a fallback for llcppg instability. This proposal uses a different boundary: LLGo controls the generation pipeline, so llcppg failures can be detected and fixed directly. A separately published wrapper repository is no longer needed as a stability layer.

The resulting ownership is:

Component Responsibility
LLAR Select and install the requested native module version and its dependencies.
LLAR metadata Provide complete pkg-config information for the installed artifact.
LLGo Orchestrate installation, dependency generation, and package discovery.
llcppg Generate Go bindings and use declared dependencies to load exported type mappings and emit Go imports.

3. Goals

  • Support llgo get <module>@<version> for LLAR modules.
  • Pass the same module and version to llar install.
  • Generate bindings into {{LLGoCacheDir}}/{{module}}@{{version}}.
  • Generate dependency bindings before the requested binding.
  • Pass versioned dependency coordinates from LLAR's structured JSON result to llcppg so it can load dependency types and generate imports.
  • Use complete pkg-config data instead of an llpkg-specific binary format.
  • Remove the need for published and independently versioned wrapper modules.

4. Proposed Workflow

4.1 Module without Dependencies

For:

llgo get madler/zlib@v1.2.1

LLGo performs the following logical steps:

  1. Run llar install madler/zlib@v1.2.1 --json.
  2. Read the resolved module version and complete pkg-config metadata.
  3. Run llcppg against the installed headers and libraries.
  4. Place the generated module at:
{{LLGoCacheDir}}/madler/zlib@v1.2.1

The generated module keeps the LLAR module name:

module madler/zlib

The generated cache entry reuses the native input version (v1.2.1). It does not have a separate wrapper release version or tag.

4.2 Import and Use the Generated Package

After:

llgo get madler/zlib@v1.2.1

an LLGo program imports the generated module by its LLAR module name:

package main

import (
	"github.com/goplus/lib/c"
	"madler/zlib"
)

func main() {
	println(c.GoString(zlib.Version()))
}

The program can then be run normally:

llgo run .

The import path is madler/zlib, not madler/zlib@v1.2.1. The version in the llgo get command selects the native artifact from which this package is generated and its versioned cache entry; it is not part of the generated Go module name or source import path.

4.3 Module with Dependencies and Generated Imports

Assume example/reader@v2.0.0 depends on madler/zlib@v1.2.1.

An illustrative LLAR JSON result is:

{
  "path": "example/reader",
  "version": "v2.0.0",
  "deps": [
    {
      "path": "madler/zlib",
      "version": "v1.2.1"
    }
  ]
}

LLGo generates the dependency first:

flowchart TD
    A["llgo get example/reader@v2.0.0"]
    B["llar install example/reader@v2.0.0 --json"]
    C["madler/zlib@v1.2.1"]
    D["Generate LLGoCacheDir/madler/zlib@v1.2.1"]
    E["Set llcppg deps to madler/zlib@v1.2.1"]
    F["Generate LLGoCacheDir/example/reader@v2.0.0"]

    A --> B
    B --> C --> D --> E --> F
Loading

LLGo supplies the LLAR dependency coordinate to llcppg without dropping its version:

{
  "deps": ["madler/zlib@v1.2.1"]
}

llcppg uses the versioned coordinate to load the generated dependency and its exported type mappings. When the generated consumer refers to a dependency type, llcppg emits the normal Go import path without @version:

import "madler/zlib"

This is why llcppg needs deps: LLAR describes which native artifacts were resolved, while llcppg needs the corresponding generated packages in order to produce cross-package type references and imports. The version belongs to dependency resolution and the cache coordinate; it never appears in Go import syntax.

4.4 Dependency Graph Rule

LLGo treats LLAR JSON as the source of the native dependency graph.

  • Dependencies are generated before their consumers.
  • The same resolved module version is generated once within one llgo get operation.
  • Diamond dependencies reuse the same generated module-and-version cache entry.
  • Direct LLAR dependency coordinates are passed to llcppg deps with their versions.
  • llcppg loads each generated dependency and emits source imports using module names without @version.

LLAR should expose direct dependencies when the result is used to populate llcppg deps. A flattened transitive closure is sufficient for installation, but does not describe the direct generated-package relationships precisely.

5. Cache and Version Model

For the coordinate madler/zlib@v1.2.1, the generated binding occupies:

{{LLGoCacheDir}}/madler/zlib@v1.2.1

This follows the Go module cache shape:

{{LLGoCacheDir}}/{{module}}@{{version}}

The version is part of the cache coordinate, but not the module directive or source import path:

cache:  madler/zlib@v1.2.1
module: madler/zlib
import: madler/zlib
Event Result
First request for madler/zlib@v1.2.1 Generate LLGoCacheDir/madler/zlib@v1.2.1.
Repeated request for the same native version Reuse that versioned cache entry when valid.
Request for madler/zlib@v1.3.1 Generate the sibling entry LLGoCacheDir/madler/zlib@v1.3.1; keep v1.2.1.
llcppg behavior changes Regenerate the affected cache entry rather than assign a new wrapper release version.

The important distinction is:

cache identity = LLAR module path + LLAR native version
independent wrapper release version = none

6. pkg-config Contract

LLAR metadata should evolve from link flags such as -lz into complete pkg-config information. LLGo then gives llcppg one standard description for both header discovery and linking.

For example, an installed artifact may provide a zlib.pc equivalent to:

prefix={{.InstallDir}}
libdir=${prefix}/lib
includedir=${prefix}/include

Name: zlib
Description: zlib compression library
Version: 1.2.1
Libs: -L${libdir} -lz
Cflags: -I${includedir}

For a module with native dependencies, Requires carries the native link relationship while LLAR JSON carries the generated-module relationship:

Name: reader
Version: 2.0.0
Requires: zlib
Libs: -L${libdir} -lreader
Cflags: -I${includedir}

The exact LLAR JSON representation of the .pc content is a separate LLAR design decision. This proposal requires only that LLGo can obtain the root pkg-config identity and make all dependency pkg-config entries available.

7. Module Naming

The LLAR module name is also the generated Go module name and forms the path part of the versioned cache coordinate:

Meaning Example
LLAR module madler/zlib
Generated Go module madler/zlib
Versioned cache directory {{LLGoCacheDir}}/madler/zlib@v1.2.1
llcppg dependency madler/zlib@v1.2.1
Generated source import madler/zlib

Module names must be safe relative paths and must not contain empty, . or .. components. Cache paths should reuse Go module path and version escaping rules rather than define another filesystem encoding. The precise package-name normalization rule is left to a follow-up proposal.

LLGo is responsible for making these generated modules discoverable during generation and compilation. The specific package-query implementation is not part of this design.

8. Key Tradeoffs and Boundary Cases

8.1 Multiple Cached Versions and One Resolved Version

Different versions of madler/zlib can coexist in the cache. For example, both of these directories may exist:

{{LLGoCacheDir}}/madler/zlib@v1.2.1
{{LLGoCacheDir}}/madler/zlib@v1.3.1

Storage does not decide which version a build imports. LLGo uses the version resolved in LLAR's dependency result when making madler/zlib discoverable to that build. Version-conflict resolution remains an LLAR dependency-graph responsibility.

8.2 Multiple pkg-config Entries

One LLAR module may install multiple .pc files. LLAR metadata must identify which pkg-config package or packages form the public binding input. The module name alone is not enough to infer this.

8.3 Native Dependencies and Binding Dependencies

A pkg-config Requires entry describes a native compilation and link relationship. An llcppg deps entry lets llcppg load another generated package's exported type mappings and emit a Go import when those types appear in the generated API.

For example, if example/reader exposes a zlib type in its public headers, LLGo passes madler/zlib@v1.2.1 to llcppg and the generated source imports madler/zlib. If a native dependency is link-only and none of its types crosses the public binding boundary, pkg-config still carries the native dependency even though the generated source may not need an import. The first implementation may conservatively pass all direct LLAR dependencies to llcppg; narrowing that set is a later improvement.

8.4 Generator Policy beyond pkg-config

pkg-config describes compilation and linking, but it does not describe llcppg-specific policy such as C++ mode, selected public headers, renamed symbols, or type mappings. This proposal does not decide where that controlled policy is stored.

8.5 Target Selection

The installed LLAR artifact and the generated binding must describe the same target. Cross-target generation should be specified separately.

9. Out of Scope

  • The exact LLAR metadata schema for complete .pc files.
  • The package-loader or workspace implementation used to find cached modules.
  • Cache locking, temporary directories, and update mechanics.
  • The storage format for generation provenance.
  • Cross-compilation behavior.
  • Automatic inference of all llcppg-specific policy.

10. Alternatives Considered

Keep llpkg as a Versioned Wrapper Repository

Rejected. It creates an additional package, version, publication, and mapping lifecycle around generated source. LLGo can instead regenerate and validate bindings directly.

Use an Unversioned Cache Path

Rejected. {{LLGoCacheDir}}/madler/zlib would make a request for one native version overwrite the binding generated for another. A Go module cache-style {{module}}@{{version}} coordinate allows both bindings to coexist without introducing an independent wrapper version.

Pass Raw Link Flags Directly to llcppg

Rejected. Link flags alone do not provide the header and compile information needed by llcppg. Complete pkg-config information supplies both through an existing standard.

11. Open Questions

  • How does LLAR identify the root .pc entry when an artifact contains more than one?
  • Does LLAR JSON expose direct dependencies or only the full artifact closure?
  • Do LLAR version strings already satisfy Go module version escaping rules, or does LLAR need a canonical cache encoding for additional version forms?
  • Where should controlled per-module llcppg policy live?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions