cmd/gobind,bind: generate complete Java interface with gobind
Output every Java class, including the support classes, from gobind
-lang=java. In addition, replace Go package export data parsing with
converting from go/ast to go/types. That way, gobind can tolerate
unknown imports as long as the exported Go API doesn't use them.
In a follow-up CL, the gobind gradle plugin will use gobind for a first
pass to expose the generated Java classes to the android plugin.
Change-Id: I8134899ec818c7fee79e4d9df8afcae9dd679add
Reviewed-on: https://go-review.googlesource.com/30093
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/bind/genjava.go b/bind/genjava.go
index 99ab7aa..c0856b0 100644
--- a/bind/genjava.go
+++ b/bind/genjava.go
@@ -816,22 +816,31 @@
"try", "void", "volatile", "while", "false", "null", "true"})
func (g *JavaGen) javaPkgName(pkg *types.Package) string {
+ return JavaPkgName(g.JavaPkg, pkg)
+}
+
+// JavaPkgName returns the Java package name for a Go package
+// given a pkg prefix. If the prefix is empty, "go" is used
+// instead.
+func JavaPkgName(pkgPrefix string, pkg *types.Package) string {
if pkg == nil {
return "go"
}
s := javaNameReplacer(pkg.Name())
- if g.JavaPkg != "" {
- return g.JavaPkg + "." + s
+ if pkgPrefix != "" {
+ return pkgPrefix + "." + s
} else {
return "go." + s
}
}
func (g *JavaGen) className() string {
- return className(g.Pkg)
+ return JavaClassName(g.Pkg)
}
-func className(pkg *types.Package) string {
+// JavaClassName returns the name of the Java class that
+// contains Go package level identifiers.
+func JavaClassName(pkg *types.Package) string {
if pkg == nil {
return "Universe"
}
@@ -1296,7 +1305,7 @@
}
for _, iface := range g.interfaces {
pkg := iface.obj.Pkg()
- g.Printf("clazz = (*env)->FindClass(env, %q);\n", g.jniClassSigPrefix(pkg)+className(pkg)+"$proxy"+iface.obj.Name())
+ g.Printf("clazz = (*env)->FindClass(env, %q);\n", g.jniClassSigPrefix(pkg)+JavaClassName(pkg)+"$proxy"+iface.obj.Name())
g.Printf("proxy_class_%s_%s = (*env)->NewGlobalRef(env, clazz);\n", g.pkgPrefix, iface.obj.Name())
g.Printf("proxy_class_%s_%s_cons = (*env)->GetMethodID(env, clazz, \"<init>\", \"(Lgo/Seq$Ref;)V\");\n", g.pkgPrefix, iface.obj.Name())
if isErrorType(iface.obj.Type()) {
@@ -1394,7 +1403,7 @@
if g.Pkg != nil {
for _, p := range g.Pkg.Imports() {
if g.validPkg(p) {
- g.Printf("%s.%s.touch();\n", g.javaPkgName(p), className(p))
+ g.Printf("%s.%s.touch();\n", g.javaPkgName(p), JavaClassName(p))
}
}
}
diff --git a/bind/testpkg/javapkg/classes.go b/bind/testpkg/javapkg/classes.go
index 891ebbe..a6545d0 100644
--- a/bind/testpkg/javapkg/classes.go
+++ b/bind/testpkg/javapkg/classes.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// +build android
+
package javapkg
import (
diff --git a/cmd/gobind/gen.go b/cmd/gobind/gen.go
index b7aa5cf..f30da5a 100644
--- a/cmd/gobind/gen.go
+++ b/cmd/gobind/gen.go
@@ -6,18 +6,24 @@
import (
"bytes"
+ "fmt"
+ "go/build"
"go/token"
"go/types"
"io"
+ "io/ioutil"
"os"
+ "os/exec"
"path/filepath"
+ "strings"
"unicode"
"unicode/utf8"
"golang.org/x/mobile/bind"
+ "golang.org/x/mobile/internal/importers/java"
)
-func genPkg(p *types.Package, allPkg []*types.Package) {
+func genPkg(p *types.Package, allPkg []*types.Package, classes []*java.Class) {
fname := defaultFileName(*lang, p)
conf := &bind.GeneratorConfig{
Fset: fset,
@@ -36,39 +42,73 @@
Pkg: conf.Pkg,
},
}
- g.Init(nil)
+ g.Init(classes)
+ pkgname := bind.JavaPkgName(*javaPkg, p)
+ pkgDir := strings.Replace(pkgname, ".", "/", -1)
buf.Reset()
- w, closer := writer(fname)
+ w, closer := writer(filepath.Join(pkgDir, fname))
processErr(g.GenJava())
io.Copy(w, &buf)
closer()
for i, name := range g.ClassNames() {
buf.Reset()
- w, closer := writer(name + ".java")
+ w, closer := writer(filepath.Join(pkgDir, name+".java"))
processErr(g.GenClass(i))
io.Copy(w, &buf)
closer()
}
buf.Reset()
- cname := "java_" + p.Name() + ".c"
+ pn := "universe"
+ if p != nil {
+ pn = p.Name()
+ }
+ cname := "java_" + pn + ".c"
w, closer = writer(cname)
processErr(g.GenC())
io.Copy(w, &buf)
closer()
buf.Reset()
- hname := p.Name() + ".h"
+ hname := pn + ".h"
w, closer = writer(hname)
processErr(g.GenH())
io.Copy(w, &buf)
closer()
+ // Generate support files along with the universe package
+ if p == nil {
+ p, err := build.Default.Import("golang.org/x/mobile/bind", ".", build.ImportComment)
+ if err != nil {
+ errorf(`"golang.org/x/mobile/bind" is not found; run go get golang.org/x/mobile/bind: %v`)
+ return
+ }
+ repo := filepath.Clean(filepath.Join(p.Dir, "..")) // golang.org/x/mobile directory.
+ for _, javaFile := range []string{"Seq.java", "LoadJNI.java"} {
+ src := filepath.Join(repo, "bind/java/"+javaFile)
+ in, err := os.Open(src)
+ if err != nil {
+ errorf("failed to open Java support file: %v", err)
+ }
+ defer in.Close()
+ w, closer := writer(filepath.Join("go", javaFile))
+ defer closer()
+ if _, err := io.Copy(w, in); err != nil {
+ errorf("failed to copy Java support file: %v", err)
+ return
+ }
+ }
+ }
case "go":
w, closer := writer(fname)
conf.Writer = w
processErr(bind.GenGo(conf))
closer()
case "objc":
- gohname := p.Name() + ".h"
+ var gohname string
+ if p != nil {
+ gohname = p.Name() + ".h"
+ } else {
+ gohname = "GoUniverse.h"
+ }
w, closer := writer(gohname)
conf.Writer = w
processErr(bind.GenObjc(conf, *prefix, bind.ObjcGoH))
@@ -87,6 +127,51 @@
}
}
+func genJavaPackages(ctx *build.Context, dir string, classes []*java.Class) error {
+ var buf bytes.Buffer
+ cg := &bind.ClassGen{
+ Printer: &bind.Printer{
+ IndentEach: []byte("\t"),
+ Buf: &buf,
+ },
+ }
+ cg.Init(classes)
+ pkgBase := filepath.Join(dir, "src", "Java")
+ if err := os.MkdirAll(pkgBase, 0700); err != nil {
+ return err
+ }
+ for i, jpkg := range cg.Packages() {
+ pkgDir := filepath.Join(pkgBase, jpkg)
+ if err := os.MkdirAll(pkgDir, 0700); err != nil {
+ return err
+ }
+ pkgFile := filepath.Join(pkgDir, "package.go")
+ buf.Reset()
+ cg.GenPackage(i)
+ if err := ioutil.WriteFile(pkgFile, buf.Bytes(), 0600); err != nil {
+ return err
+ }
+ }
+ buf.Reset()
+ cg.GenInterfaces()
+ clsFile := filepath.Join(pkgBase, "interfaces.go")
+ if err := ioutil.WriteFile(clsFile, buf.Bytes(), 0600); err != nil {
+ return err
+ }
+
+ cmd := exec.Command(
+ "go",
+ "install",
+ "-pkgdir="+filepath.Join(dir, "pkg", ctx.GOOS+"_"+ctx.GOARCH),
+ "Java/...",
+ )
+ cmd.Env = append(cmd.Env, "GOPATH="+dir)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("failed to go install the generated Java wrappers: %v: %s", err, string(out))
+ }
+ return nil
+}
+
func processErr(err error) {
if err != nil {
if list, _ := err.(bind.ErrorList); len(list) > 0 {
@@ -129,12 +214,21 @@
func defaultFileName(lang string, pkg *types.Package) string {
switch lang {
case "java":
+ if pkg == nil {
+ return "Universe.java"
+ }
firstRune, size := utf8.DecodeRuneInString(pkg.Name())
className := string(unicode.ToUpper(firstRune)) + pkg.Name()[size:]
return className + ".java"
case "go":
+ if pkg == nil {
+ return "go_universe.go"
+ }
return "go_" + pkg.Name() + ".go"
case "objc":
+ if pkg == nil {
+ return "GoUniverse.m"
+ }
firstRune, size := utf8.DecodeRuneInString(pkg.Name())
className := string(unicode.ToUpper(firstRune)) + pkg.Name()[size:]
return "Go" + className + ".m"
diff --git a/cmd/gobind/gobind_test.go b/cmd/gobind/gobind_test.go
new file mode 100644
index 0000000..873ffde
--- /dev/null
+++ b/cmd/gobind/gobind_test.go
@@ -0,0 +1,66 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "os/exec"
+ "testing"
+)
+
+var tests = []struct {
+ name string
+ lang string
+ pkg string
+}{
+ {"ObjC-Testpkg", "objc", "golang.org/x/mobile/bind/testpkg"},
+ {"Java-Testpkg", "java", "golang.org/x/mobile/bind/testpkg"},
+ {"Go-Testpkg", "go", "golang.org/x/mobile/bind/testpkg"},
+ {"Java-Javapkg", "java", "golang.org/x/mobile/bind/testpkg/javapkg"},
+ {"Go-Javapkg", "go", "golang.org/x/mobile/bind/testpkg/javapkg"},
+}
+
+func installGobind() error {
+ if out, err := exec.Command("go", "install", "golang.org/x/mobile/cmd/gobind").CombinedOutput(); err != nil {
+ return fmt.Errorf("gobind install failed: %v: %s", err, out)
+ }
+ return nil
+}
+
+func runGobind(lang, pkg string) error {
+ cmd := exec.Command("gobind", "-lang", lang, pkg)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("gobind -lang %s %s failed: %v: %s", lang, pkg, err, out)
+ }
+ return nil
+}
+
+func TestGobind(t *testing.T) {
+ if err := installGobind(); err != nil {
+ t.Fatal(err)
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ if err := runGobind(test.lang, test.pkg); err != nil {
+ t.Error(err)
+ }
+ })
+ }
+}
+
+func BenchmarkGobind(b *testing.B) {
+ if err := installGobind(); err != nil {
+ b.Fatal(err)
+ }
+ for _, test := range tests {
+ b.Run(test.name, func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ if err := runGobind(test.lang, test.pkg); err != nil {
+ b.Error(err)
+ }
+ }
+ })
+ }
+}
diff --git a/cmd/gobind/main.go b/cmd/gobind/main.go
index c0d2f9b..ef6e808 100644
--- a/cmd/gobind/main.go
+++ b/cmd/gobind/main.go
@@ -7,19 +7,28 @@
import (
"flag"
"fmt"
+ "go/ast"
+ "go/build"
"go/importer"
+ "go/parser"
+ "go/token"
"go/types"
+ "io/ioutil"
"log"
"os"
- "os/exec"
- "strings"
+ "path/filepath"
+
+ "golang.org/x/mobile/internal/importers"
+ "golang.org/x/mobile/internal/importers/java"
)
var (
- lang = flag.String("lang", "java", "target language for bindings, either java, go, or objc (experimental).")
- outdir = flag.String("outdir", "", "result will be written to the directory instead of stdout.")
- javaPkg = flag.String("javapkg", "", "custom Java package path prefix used instead of the default 'go'. Valid only with -lang=java.")
- prefix = flag.String("prefix", "", "custom Objective-C name prefix used instead of the default 'Go'. Valid only with -lang=objc.")
+ lang = flag.String("lang", "java", "target language for bindings, either java, go, or objc (experimental).")
+ outdir = flag.String("outdir", "", "result will be written to the directory instead of stdout.")
+ javaPkg = flag.String("javapkg", "", "custom Java package path prefix used instead of the default 'go'. Valid only with -lang=java.")
+ prefix = flag.String("prefix", "", "custom Objective-C name prefix used instead of the default 'Go'. Valid only with -lang=objc.")
+ bootclasspath = flag.String("bootclasspath", "", "Java bootstrap classpath.")
+ classpath = flag.String("classpath", "", "Java classpath.")
)
var usage = `The Gobind tool generates Java language bindings for Go.
@@ -35,31 +44,70 @@
log.Fatalf("Invalid option -prefix for gobind -lang=%s", *lang)
}
- // Make sure the export data for the packages being compiled is up to
- // date. Also use the go tool to provide good error messages for any
- // type checking errors in the provided packages.
- cmd := exec.Command("go", "install")
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Args = append(cmd.Args, flag.Args()...)
- if err := cmd.Run(); err != nil {
- fmt.Fprintf(os.Stderr, "%s failed: %v", strings.Join(cmd.Args, " "), err)
- os.Exit(1)
- }
-
- var allPkg []*types.Package
- imp := importer.Default()
- for _, arg := range flag.Args() {
- pkg, err := imp.Import(arg)
+ oldCtx := build.Default
+ ctx := &build.Default
+ var allPkg []*build.Package
+ for _, path := range flag.Args() {
+ pkg, err := ctx.Import(path, ".", build.ImportComment)
if err != nil {
- fmt.Fprintf(os.Stderr, "could not import package %s: %v", arg, err)
- os.Exit(1)
+ log.Fatalf("package %q: %v", path, err)
}
allPkg = append(allPkg, pkg)
}
- for _, pkg := range allPkg {
- genPkg(pkg, allPkg)
+ var classes []*java.Class
+ refs, err := importers.AnalyzePackages(allPkg, "Java/")
+ if err != nil {
+ log.Fatal(err)
}
+ if len(refs.Refs) > 0 {
+ classes, err = java.Import(*bootclasspath, *classpath, refs)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if len(classes) > 0 {
+ tmpGopath, err := ioutil.TempDir(os.TempDir(), "gobind-")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer os.RemoveAll(tmpGopath)
+ if err := genJavaPackages(ctx, tmpGopath, classes); err != nil {
+ log.Fatal(err)
+ }
+ gopath := ctx.GOPATH
+ if gopath != "" {
+ gopath = string(filepath.ListSeparator)
+ }
+ ctx.GOPATH = gopath + tmpGopath
+ }
+ }
+
+ typePkgs := make([]*types.Package, len(allPkg))
+ fset := token.NewFileSet()
+ conf := &types.Config{
+ Importer: importer.Default(),
+ }
+ conf.Error = func(err error) {
+ // Ignore errors. They're probably caused by as-yet undefined
+ // Java wrappers.
+ }
+ for i, pkg := range allPkg {
+ var files []*ast.File
+ for _, name := range pkg.GoFiles {
+ f, err := parser.ParseFile(fset, filepath.Join(pkg.Dir, name), nil, 0)
+ if err != nil {
+ log.Fatalf("Failed to parse Go file %s: %v", name, err)
+ }
+ files = append(files, f)
+ }
+ tpkg, _ := conf.Check(pkg.Name, fset, files, nil)
+ typePkgs[i] = tpkg
+ }
+ build.Default = oldCtx
+ for _, pkg := range typePkgs {
+ genPkg(pkg, typePkgs, classes)
+ }
+ // Generate the error package and support files
+ genPkg(nil, typePkgs, classes)
os.Exit(exitStatus)
}
diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go
index 1659d8b..963f0c2 100644
--- a/cmd/gomobile/bind.go
+++ b/cmd/gomobile/bind.go
@@ -364,7 +364,9 @@
return classes, nil
}
-func (b *binder) GenJava(pkg *types.Package, allPkg []*types.Package, classes []*java.Class, outdir, javadir string) error {
+func (b *binder) GenJava(pkg *types.Package, allPkg []*types.Package, classes []*java.Class, outdir, androidDir string) error {
+ jpkgname := bind.JavaPkgName(bindJavaPkg, pkg)
+ javadir := filepath.Join(androidDir, strings.Replace(jpkgname, ".", "/", -1))
var className string
pkgName := ""
pkgPath := ""
diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go
index cad67c5..cb86f00 100644
--- a/cmd/gomobile/bind_androidapp.go
+++ b/cmd/gomobile/bind_androidapp.go
@@ -108,17 +108,13 @@
}
repo := filepath.Clean(filepath.Join(p.Dir, "..")) // golang.org/x/mobile directory.
+ jclsDir := filepath.Join(androidDir, "src", "main", "java")
for _, pkg := range binder.pkgs {
- pkgpath := bindJavaPkg
- if pkgpath == "" {
- pkgpath = "go." + pkg.Name()
- }
- jclsDir := filepath.Join(androidDir, "src/main/java/"+strings.Replace(pkgpath, ".", "/", -1))
if err := binder.GenJava(pkg, binder.pkgs, classes, srcDir, jclsDir); err != nil {
return err
}
}
- if err := binder.GenJava(nil, binder.pkgs, classes, srcDir, filepath.Join(androidDir, "src/main/java/go")); err != nil {
+ if err := binder.GenJava(nil, binder.pkgs, classes, srcDir, jclsDir); err != nil {
return err
}
if err := binder.GenJavaSupport(srcDir); err != nil {
diff --git a/cmd/gomobile/bind_test.go b/cmd/gomobile/bind_test.go
index 0e4e708..4f19be6 100644
--- a/cmd/gomobile/bind_test.go
+++ b/cmd/gomobile/bind_test.go
@@ -50,7 +50,7 @@
{
javaPkg: "com.example.foo",
wantGobind: "gobind -lang=java -javapkg=com.example.foo",
- wantPkgDir: "com/example/foo",
+ wantPkgDir: "com/example/foo/asset",
},
}
for _, tc := range tests {