From 83237843f263cacbbb8202d4adee05bd79321e77 Mon Sep 17 00:00:00 2001 From: Arthur Fabre Date: Tue, 4 Aug 2026 15:27:56 +0200 Subject: [PATCH 1/2] clang: CompileRes() to return CPU time used by clang binary Expose the total system plus userspace time used by clang, to allow users to monitor how much of their CPU use can be attributed to clang. Add a new CompileRes() API to avoid breaking existing users. --- clang/clang.go | 40 +++++++++++++++++++++++++++++----------- clang/clang_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 clang/clang_test.go diff --git a/clang/clang.go b/clang/clang.go index 69390a4..d774fd5 100644 --- a/clang/clang.go +++ b/clang/clang.go @@ -9,11 +9,12 @@ import ( "os/exec" "path/filepath" "strings" + "time" "github.com/pkg/errors" ) -// CompileOpts configure how an XDP program is compiled / built +// Opts configure how an XDP program is compiled / built type Opts struct { // clang binary to use Clang string @@ -30,15 +31,23 @@ type Opts struct { EmitDebug bool } -// Compile compiles a C source string into an ELF -func Compile(source []byte, name string, opts Opts) ([]byte, error) { +// Res is the result of compiling a C source with clang. +type Res struct { + ELF []byte + + // CPUTime is the user + system CPU time used by this clang invocation. + CPUTime time.Duration +} + +// Compile compiles a C source string into an ELF, and returns metadata in Res. +func CompileRes(source []byte, name string, opts Opts) (Res, error) { var err error outdir := opts.Output if outdir == "" { outdir, err = ioutil.TempDir("", "cbpfc-clang") if err != nil { - return nil, errors.Wrap(err, "can't create output directory") + return Res{}, errors.Wrap(err, "can't create output directory") } defer os.RemoveAll(outdir) } else { @@ -49,7 +58,7 @@ func Compile(source []byte, name string, opts Opts) ([]byte, error) { outputFile := fmt.Sprintf("%s.elf", name) err = ioutil.WriteFile(filepath.Join(outdir, inputFile), source, 0644) if err != nil { - return nil, errors.Wrap(err, "can't write out program") + return Res{}, errors.Wrap(err, "can't write out program") } flags := []string{ @@ -66,7 +75,7 @@ func Compile(source []byte, name string, opts Opts) ([]byte, error) { // debug build script will be in a different directory, relative imports won't work absInclude, err := filepath.Abs(include) if err != nil { - return nil, errors.Wrapf(err, "can't get absolute path to include %s", include) + return Res{}, errors.Wrapf(err, "can't get absolute path to include %s", include) } flags = append(flags, "-I", absInclude) @@ -83,7 +92,7 @@ func Compile(source []byte, name string, opts Opts) ([]byte, error) { cmdline := cmd.Path + " " + strings.Join(flags, " ") + "\n" err := ioutil.WriteFile(filepath.Join(outdir, "build"), []byte(cmdline), 0644) if err != nil { - return nil, errors.Wrap(err, "can't write build cmdline") + return Res{}, errors.Wrap(err, "can't write build cmdline") } } @@ -92,16 +101,25 @@ func Compile(source []byte, name string, opts Opts) ([]byte, error) { if err != nil { switch e := err.(type) { case *exec.ExitError: - return nil, errors.Wrapf(e, "unable to compile C:\n%s", string(e.Stderr)) + return Res{}, errors.Wrapf(e, "unable to compile C:\n%s", string(e.Stderr)) default: - return nil, errors.Wrapf(e, "unable to compile C") + return Res{}, errors.Wrapf(e, "unable to compile C") } } elf, err := ioutil.ReadFile(filepath.Join(outdir, outputFile)) if err != nil { - return nil, errors.Wrap(err, "can't read ELF") + return Res{}, errors.Wrap(err, "can't read ELF") } - return elf, nil + return Res{ + ELF: elf, + CPUTime: cmd.ProcessState.SystemTime() + cmd.ProcessState.UserTime(), + }, nil +} + +// Compile compiles a C source string into an ELF. +func Compile(source []byte, name string, opts Opts) ([]byte, error) { + res, err := CompileRes(source, name, opts) + return res.ELF, err } diff --git a/clang/clang_test.go b/clang/clang_test.go new file mode 100644 index 0000000..09bae33 --- /dev/null +++ b/clang/clang_test.go @@ -0,0 +1,43 @@ +package clang + +import ( + "os" + "testing" +) + +func TestCompile(t *testing.T) { + clangBin, ok := os.LookupEnv("CLANG") + if !ok { + clangBin = "/usr/bin/clang" + } + + source := []byte(`int main() { return 0; }`) + opts := Opts{ + Clang: clangBin, + } + + t.Run("bare", func(t *testing.T) { + elf, err := Compile(source, "test", opts) + if err != nil { + t.Fatalf("error: %v", err) + } + + if len(elf) == 0 { + t.Fatal("ELF empty") + } + }) + + t.Run("res", func(t *testing.T) { + res, err := CompileRes(source, "test", opts) + if err != nil { + t.Fatalf("error: %v", err) + } + + if len(res.ELF) == 0 { + t.Fatal("ELF empty") + } + if res.CPUTime == 0 { + t.Fatal("CPUTime zero") + } + }) +} From bad263e8545984100574fa790c581a915ae74531 Mon Sep 17 00:00:00 2001 From: Arthur Fabre Date: Tue, 4 Aug 2026 17:51:42 +0200 Subject: [PATCH 2/2] clang: Use stdin / stdout for clang without Output directory If the opts.Output directory isn't set, use stdin to pass the source to clang, and read the ELF from stdout. This avoids needing to write to temporary files. --- clang/clang.go | 83 +++++++++++++++++++++++++-------------------- clang/clang_test.go | 33 ++++++++++++++++++ 2 files changed, 80 insertions(+), 36 deletions(-) diff --git a/clang/clang.go b/clang/clang.go index d774fd5..15a77d2 100644 --- a/clang/clang.go +++ b/clang/clang.go @@ -3,8 +3,8 @@ package clang import ( + "bytes" "fmt" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -23,7 +23,7 @@ type Opts struct { Include []string // Destination directory for compiled programs. - // Uses a temporary directory if empty. + // Uses stdout if empty. Output string // Emit DWARF debug info in the XDP elf. @@ -39,65 +39,77 @@ type Res struct { CPUTime time.Duration } -// Compile compiles a C source string into an ELF, and returns metadata in Res. -func CompileRes(source []byte, name string, opts Opts) (Res, error) { - var err error - - outdir := opts.Output - if outdir == "" { - outdir, err = ioutil.TempDir("", "cbpfc-clang") - if err != nil { - return Res{}, errors.Wrap(err, "can't create output directory") - } - defer os.RemoveAll(outdir) - } else { - _ = os.Mkdir(outdir, 0755) - } - - inputFile := fmt.Sprintf("%s.c", name) - outputFile := fmt.Sprintf("%s.elf", name) - err = ioutil.WriteFile(filepath.Join(outdir, inputFile), source, 0644) - if err != nil { - return Res{}, errors.Wrap(err, "can't write out program") - } - +func (o Opts) cmd(inputFile string, outputFile string) (*exec.Cmd, error) { flags := []string{ "-O2", "-Wall", "-Werror", "-nostdinc", "-c", "-target", "bpf", - inputFile, + // read C source (to support stdin) + "-x", "c", inputFile, + // output "-o", outputFile, } - for _, include := range opts.Include { + for _, include := range o.Include { // debug build script will be in a different directory, relative imports won't work absInclude, err := filepath.Abs(include) if err != nil { - return Res{}, errors.Wrapf(err, "can't get absolute path to include %s", include) + return nil, errors.Wrapf(err, "can't get absolute path to include %s", include) } flags = append(flags, "-I", absInclude) } - if opts.EmitDebug { + if o.EmitDebug { flags = append(flags, "-g") } - cmd := exec.Command(opts.Clang, flags...) + return exec.Command(o.Clang, flags...), nil +} + +// Compile compiles a C source string into an ELF, and returns metadata in Res. +func CompileRes(source []byte, name string, opts Opts) (Res, error) { + // Use stdout if no output dir is set to avoid a temporary file. + input := "-" + output := "-" + outputFunc := func(stdout []byte) ([]byte, error) { + return stdout, nil + } + if opts.Output != "" { + _ = os.Mkdir(opts.Output, 0755) + input = filepath.Join(opts.Output, fmt.Sprintf("%s.c", name)) + if err := os.WriteFile(input, source, 0644); err != nil { + return Res{}, err + } + output = filepath.Join(opts.Output, fmt.Sprintf("%s.elf", name)) + outputFunc = func(stdout []byte) ([]byte, error) { + return os.ReadFile(output) + } + } + + cmd, err := opts.cmd(input, output) + if err != nil { + return Res{}, err + } // debug build script if opts.Output != "" { - cmdline := cmd.Path + " " + strings.Join(flags, " ") + "\n" - err := ioutil.WriteFile(filepath.Join(outdir, "build"), []byte(cmdline), 0644) + cmdline := cmd.Path + " " + strings.Join(cmd.Args, " ") + "\n" + err := os.WriteFile(filepath.Join(opts.Output, "build"), []byte(cmdline), 0644) if err != nil { return Res{}, errors.Wrap(err, "can't write build cmdline") } + } else { + cmd.Stdin = bytes.NewReader(source) } - cmd.Dir = outdir - _, err = cmd.Output() + return compileRes(cmd, outputFunc) +} + +func compileRes(cmd *exec.Cmd, output func(stdout []byte) ([]byte, error)) (Res, error) { + stdout, err := cmd.Output() if err != nil { switch e := err.(type) { case *exec.ExitError: @@ -106,10 +118,9 @@ func CompileRes(source []byte, name string, opts Opts) (Res, error) { return Res{}, errors.Wrapf(e, "unable to compile C") } } - - elf, err := ioutil.ReadFile(filepath.Join(outdir, outputFile)) + elf, err := output(stdout) if err != nil { - return Res{}, errors.Wrap(err, "can't read ELF") + return Res{}, err } return Res{ diff --git a/clang/clang_test.go b/clang/clang_test.go index 09bae33..9ba2c40 100644 --- a/clang/clang_test.go +++ b/clang/clang_test.go @@ -1,7 +1,10 @@ package clang import ( + "bytes" "os" + "path/filepath" + "strings" "testing" ) @@ -40,4 +43,34 @@ func TestCompile(t *testing.T) { t.Fatal("CPUTime zero") } }) + + t.Run("output_dir", func(t *testing.T) { + opts := opts + + output := t.TempDir() + opts.Output = output + + elf, err := Compile(source, "test", opts) + if err != nil { + t.Fatalf("error: %v", err) + } + + if !bytes.Equal(source, mustReadFile(t, output, "test.c")) { + t.Fatal("source doesn't match") + } + if !bytes.Equal(elf, mustReadFile(t, output, "test.elf")) { + t.Fatal("Returned ELF and filesystem ELF are different") + } + if build := string(mustReadFile(t, output, "build")); !strings.Contains(build, "clang") { + t.Fatalf("bad build script: %q", build) + } + }) +} + +func mustReadFile(tb testing.TB, dir string, file string) []byte { + val, err := os.ReadFile(filepath.Join(dir, file)) + if err != nil { + tb.Fatal(err) + } + return val }