Skip to content

Commit 92039d4

Browse files
demosdemonCopilot
andauthored
feat(ffi): automatically generate cgo pragma declarations (#1784)
## Why this should be merged Closes: #1774 ## How this works This change adds a `go generate` program to parse the go source files for `C.func(...)` calls and injects the relevant `#cgo` pragma comments into those files. This also removes any previous pragmas. Also added is a check to ensure that each c function has at most 1 callsite. While technically out of scope, it was necessary to ensure we don't inject multiple pragmas. However, it is also unexpected if we were to invoke a c function from two different places. ### Why not in Rust? Go already has the proper tools to parse itself. ## How this was tested The `ffi-check-go-generate` job was added to the CI workflow to ensure that `go generate` does not produce any changes. It will fail very early if so. https://github.com/ava-labs/firewood/actions/runs/23062649367/job/66993031048?pr=1784#step:3:202 ## Breaking Changes n/a --------- Signed-off-by: Joachim Brandon LeBlanc <brandon@leblanc.codes> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 913a26b commit 92039d4

File tree

11 files changed

+414
-35
lines changed

11 files changed

+414
-35
lines changed

.github/workflows/ci.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,22 @@ jobs:
224224
- name: doc generation
225225
run: RUSTDOCFLAGS="-D warnings" cargo doc --locked --document-private-items --no-deps
226226

227+
ffi-check-go-generate:
228+
runs-on: ubuntu-latest
229+
steps:
230+
- uses: actions/checkout@v6
231+
- name: Check `go generate` for changes
232+
working-directory: ffi
233+
run: |
234+
go generate
235+
if [[ -n $(git status --porcelain) ]]; then
236+
echo "go generate resulted in changes to tracked files. Please commit these changes."
237+
git --no-pager diff
238+
exit 1
239+
fi
240+
227241
ffi:
228-
needs: build
242+
needs: [build, ffi-check-go-generate]
229243
runs-on: ${{ matrix.os }}
230244
strategy:
231245
matrix:

ffi/firewood.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
// [Firewood]: https://github.com/ava-labs/firewood
77
package ffi
88

9-
//go:generate go run generate_cgo.go
9+
//go:generate go run ./gen/update-cgo-ldflags
10+
//go:generate go run ./gen/update-cgo-pragmas
1011

1112
// // Note that -lm is required on Linux but not on Mac.
1213
// // FIREWOOD_CGO_BEGIN_STATIC_LIBS
Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
22
// See the file LICENSE.md for licensing terms.
33

4-
//go:build ignore
5-
64
// go generate script
75
//
86
// This script fixes up a go file to enable/disable the correct cgo directives,
@@ -41,10 +39,6 @@ import (
4139
"strings"
4240
)
4341

44-
const (
45-
defaultMode = "LOCAL_LIBS"
46-
)
47-
4842
var errGoFileNotSet = errors.New("GOFILE is not set")
4943

5044
func main() {
@@ -81,6 +75,8 @@ func getTargetFile() (string, error) {
8175
}
8276

8377
func changeCgoDirectivesForFile(targetMode string, targetFile string) error {
78+
const none = "None"
79+
8480
originalFileContent, err := os.ReadFile(targetFile)
8581
if err != nil {
8682
return fmt.Errorf("failed to read %s: %w", targetFile, err)
@@ -89,25 +85,25 @@ func changeCgoDirectivesForFile(targetMode string, targetFile string) error {
8985
fileLines := strings.Split(string(originalFileContent), "\n")
9086

9187
// Initial state is "None" which does not process any lines
92-
currentBlockName := "None"
88+
currentBlockName := none
9389
for i, line := range fileLines {
9490
// process state transitions
9591
// if the line starts with "// FIREWOOD_CGO_BEGIN_", set the state to the text after the prefix
9692
if newBlockName, ok := strings.CutPrefix(line, "// // FIREWOOD_CGO_BEGIN_"); ok {
97-
if currentBlockName != "None" {
93+
if currentBlockName != none {
9894
return fmt.Errorf("[ERROR] %s:%d: nested CGO blocks not allowed (found %s after %s)", targetFile, i+1, newBlockName, currentBlockName)
9995
}
10096
currentBlockName = newBlockName
10197
continue
102-
} else if line == fmt.Sprintf("// // FIREWOOD_CGO_END_%s", currentBlockName) {
103-
currentBlockName = "None"
98+
} else if line == "// // FIREWOOD_CGO_END_"+currentBlockName {
99+
currentBlockName = none
104100
continue
105101
}
106102

107103
// If we are in a block, process the line
108-
if currentBlockName != "None" {
104+
if currentBlockName != none {
109105
if !isCGODirective(line) {
110-
return fmt.Errorf("[ERROR] %s:%d: invalid CGO directive in %s section:\n===\n%s\n===\n", targetFile, i+1, currentBlockName, line)
106+
return fmt.Errorf("[ERROR] %s:%d: invalid CGO directive in %s section:\n===\n%s\n===", targetFile, i+1, currentBlockName, line)
111107
}
112108
if currentBlockName == targetMode {
113109
fileLines[i] = activateCGOLine(fileLines[i])
@@ -117,7 +113,7 @@ func changeCgoDirectivesForFile(targetMode string, targetFile string) error {
117113
}
118114
}
119115

120-
if currentBlockName != "None" {
116+
if currentBlockName != none {
121117
return fmt.Errorf("[ERROR] %s: unterminated CGO block ended in %s", targetFile, currentBlockName)
122118
}
123119

@@ -127,6 +123,9 @@ func changeCgoDirectivesForFile(targetMode string, targetFile string) error {
127123
fmt.Printf("[INFO] No changes needed to %s\n", targetFile)
128124
return nil
129125
}
126+
// #nosec G306 - permissions are correct for source files
127+
// #nosec G703 - path is safe because it is controlled by the build system
128+
// and not user input
130129
return os.WriteFile(targetFile, []byte(newContents), 0o644)
131130
}
132131

0 commit comments

Comments
 (0)