Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 67 additions & 38 deletions clang/clang.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,47 +23,36 @@ 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.
// Required for BTF.
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 {
Expand All @@ -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
}
76 changes: 76 additions & 0 deletions clang/clang_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading