diff --git a/clang/clang.go b/clang/clang.go index 69390a4..15a77d2 100644 --- a/clang/clang.go +++ b/clang/clang.go @@ -3,17 +3,18 @@ package clang import ( + "bytes" "fmt" - "io/ioutil" "os" "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 @@ -22,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. @@ -30,39 +31,28 @@ type Opts struct { EmitDebug bool } -// Compile compiles a C source string into an ELF -func Compile(source []byte, name string, opts Opts) ([]byte, error) { - var err error +// Res is the result of compiling a C source with clang. +type Res struct { + ELF []byte - outdir := opts.Output - if outdir == "" { - outdir, err = ioutil.TempDir("", "cbpfc-clang") - if err != nil { - return nil, 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 nil, errors.Wrap(err, "can't write out program") - } + // CPUTime is the user + system CPU time used by this clang invocation. + CPUTime time.Duration +} +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 { @@ -72,36 +62,75 @@ func Compile(source []byte, name string, opts Opts) ([]byte, error) { 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 nil, errors.Wrap(err, "can't write build cmdline") + 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: - 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)) + elf, err := output(stdout) if err != nil { - return nil, errors.Wrap(err, "can't read ELF") + return Res{}, err } - 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..9ba2c40 --- /dev/null +++ b/clang/clang_test.go @@ -0,0 +1,76 @@ +package clang + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "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") + } + }) + + 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 +}