diff --git a/bind/genjava.go b/bind/genjava.go index b197640aa..01cea472d 100644 --- a/bind/genjava.go +++ b/bind/genjava.go @@ -1713,7 +1713,6 @@ import go.Seq; // // autogenerated by gobind %[1]s %[2]s -#include #include #include "seq.h" #include "_cgo_export.h" diff --git a/bind/java/NativeUtils.java b/bind/java/NativeUtils.java new file mode 100644 index 000000000..cfa123d56 --- /dev/null +++ b/bind/java/NativeUtils.java @@ -0,0 +1,142 @@ +/* + * Class NativeUtils is published under the The MIT License: + * + * Copyright (c) 2012 Adam Heinrich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package go; + +import java.io.*; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.ProviderNotFoundException; +import java.nio.file.StandardCopyOption; + +/** + * A simple library class which helps with loading dynamic libraries stored in the + * JAR archive. These libraries usually contain implementation of some methods in + * native code (using JNI - Java Native Interface). + * + * @see http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar + * @see https://github.com/adamheinrich/native-utils + * + */ +public class NativeUtils { + + /** + * The minimum length a prefix for a file has to have according to {@link File#createTempFile(String, String)}}. + */ + private static final int MIN_PREFIX_LENGTH = 3; + public static final String NATIVE_FOLDER_PATH_PREFIX = "nativeutils"; + + /** + * Temporary directory which will contain the DLLs. + */ + private static File temporaryDir; + + /** + * Private constructor - this class will never be instanced + */ + private NativeUtils() { + } + + /** + * Loads library from current JAR archive + * + * The file from JAR is copied into system temporary directory and then loaded. The temporary file is deleted after + * exiting. + * Method uses String as filename because the pathname is "abstract", not system-dependent. + * + * @param path The path of file inside JAR as absolute path (beginning with '/'), e.g. /package/File.ext + * @throws IOException If temporary file creation or read/write operation fails + * @throws IllegalArgumentException If source file (param path) does not exist + * @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than three characters + * (restriction of {@link File#createTempFile(java.lang.String, java.lang.String)}). + * @throws FileNotFoundException If the file could not be found inside the JAR. + */ + public static void loadLibraryFromJar(String path) throws IOException { + + if (null == path || !path.startsWith("/")) { + throw new IllegalArgumentException("The path has to be absolute (start with '/')."); + } + + // Obtain filename from path + String[] parts = path.split("/"); + String filename = (parts.length > 1) ? parts[parts.length - 1] : null; + + // Check if the filename is okay + if (filename == null || filename.length() < MIN_PREFIX_LENGTH) { + throw new IllegalArgumentException("The filename has to be at least 3 characters long."); + } + + // Prepare temporary file + if (temporaryDir == null) { + temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX); + temporaryDir.deleteOnExit(); + } + + File temp = new File(temporaryDir, filename); + + try (InputStream is = NativeUtils.class.getResourceAsStream(path)) { + Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + temp.delete(); + throw e; + } catch (NullPointerException e) { + temp.delete(); + throw new FileNotFoundException("File " + path + " was not found inside JAR."); + } + + try { + System.load(temp.getAbsolutePath()); + } finally { + if (isPosixCompliant()) { + // Assume POSIX compliant file system, can be deleted after loading + temp.delete(); + } else { + // Assume non-POSIX, and don't delete until last file descriptor closed + temp.deleteOnExit(); + } + } + } + + private static boolean isPosixCompliant() { + try { + return FileSystems.getDefault() + .supportedFileAttributeViews() + .contains("posix"); + } catch (FileSystemNotFoundException + | ProviderNotFoundException + | SecurityException e) { + return false; + } + } + + private static File createTempDirectory(String prefix) throws IOException { + String tempDir = System.getProperty("java.io.tmpdir"); + File generatedDir = new File(tempDir, prefix + System.nanoTime()); + + if (!generatedDir.mkdir()) + throw new IOException("Failed to create temp directory " + generatedDir.getName()); + + return generatedDir; + } +} diff --git a/bind/java/Seq.java b/bind/java/Seq.java index 367acdb67..9e9388823 100644 --- a/bind/java/Seq.java +++ b/bind/java/Seq.java @@ -4,7 +4,7 @@ package go; -import android.content.Context; +// import android.content.Context; import java.lang.ref.PhantomReference; import java.lang.ref.Reference; @@ -34,15 +34,30 @@ public class Seq { private static final GoRefQueue goRefQueue = new GoRefQueue(); static { - System.loadLibrary("gojni"); + if ("The Android Project".equals(System.getProperty("java.vendor"))) { + System.loadLibrary("gojni"); + } else { + String arch = System.getProperty("os.arch"); + if ("aarch64".equals(arch)) { + arch = "arm64"; + } else if ("x86_64".equals(arch)) { + arch = "amd64"; + } + try { + NativeUtils.loadLibraryFromJar("/jniLibs/" + arch + "/libgojni.so"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } init(); Universe.touch(); } + // TODO we probably need to make this file go templatable, to support plain JVM and Android better. // setContext sets the context in the go-library to be used in RunOnJvm. - public static void setContext(Context context) { - setContext((java.lang.Object)context); - } + // public static void setContext(Context context) { + // setContext((java.lang.Object)context); + // } private static native void init(); diff --git a/bind/java/context_android.c b/bind/java/context_android.c index 66ce56915..cfb457429 100644 --- a/bind/java/context_android.c +++ b/bind/java/context_android.c @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. #include -#include "seq_android.h" +#include "seq_java.h" #include "_cgo_export.h" JNIEXPORT void JNICALL diff --git a/bind/java/seq_android.c.support b/bind/java/seq_android.c.support index 77ec5f4ae..9fd08ec4c 100644 --- a/bind/java/seq_android.c.support +++ b/bind/java/seq_android.c.support @@ -6,7 +6,6 @@ // generated gomobile_bind package and compiled along with the // generated binding files. -#include #include #include #include @@ -19,6 +18,12 @@ #define NULL_REFNUM 41 +#ifndef __GOBIND_ANDROID__ +#define ACT_ENV_CAST (void**) +#else +#define ACT_ENV_CAST +#endif + // initClasses are only exported from Go if reverse bindings are used. // If they're not, weakly define a no-op function. __attribute__((weak)) void initClasses(void) { } @@ -55,7 +60,7 @@ static JNIEnv *go_seq_get_thread_env(void) { if (ret != JNI_EDETACHED) { LOG_FATAL("failed to get thread env"); } - if ((*jvm)->AttachCurrentThread(jvm, &env, NULL) != JNI_OK) { + if ((*jvm)->AttachCurrentThread(jvm, ACT_ENV_CAST &env, NULL) != JNI_OK) { LOG_FATAL("failed to attach current thread"); } pthread_setspecific(jnienvs, env); diff --git a/bind/java/seq_android.go.support b/bind/java/seq_android.go.support index a83229268..3729ddcff 100644 --- a/bind/java/seq_android.go.support +++ b/bind/java/seq_android.go.support @@ -9,11 +9,11 @@ package main // files. //#cgo CFLAGS: -Werror -//#cgo LDFLAGS: -llog +//#cgo android LDFLAGS: -llog //#include //#include //#include -//#include "seq_android.h" +//#include "seq_java.h" import "C" import ( "unsafe" diff --git a/bind/java/seq_android.h b/bind/java/seq_android.h index 26e90251e..9e0615ed6 100644 --- a/bind/java/seq_android.h +++ b/bind/java/seq_android.h @@ -6,11 +6,14 @@ #define __GO_SEQ_ANDROID_HDR__ #include -#include // For abort() #include #include +#ifdef __GOBIND_ANDROID__ + +#include + #define LOG_INFO(...) __android_log_print(ANDROID_LOG_INFO, "go/Seq", __VA_ARGS__) #define LOG_FATAL(...) \ { \ @@ -18,6 +21,17 @@ abort(); \ } +#else + +#define LOG_INFO(...) printf(__VA_ARGS__) +#define LOG_FATAL(...) \ + { \ + fprintf(stderr, __VA_ARGS__); \ + abort(); \ + } + +#endif + // Platform specific types typedef struct nstring { // UTF16 or UTF8 Encoded string. When converting from Java string to Go diff --git a/bind/seq.go.support b/bind/seq.go.support index 392ec09c0..7fa5ec0a3 100644 --- a/bind/seq.go.support +++ b/bind/seq.go.support @@ -9,6 +9,7 @@ package main // with the bindings. // #cgo android CFLAGS: -D__GOBIND_ANDROID__ +// #cgo java CFLAGS: -D__GOBIND_JAVA__ // #cgo darwin CFLAGS: -D__GOBIND_DARWIN__ // #include // #include "seq.h" diff --git a/cmd/gobind/gen.go b/cmd/gobind/gen.go index fedbc2705..082810419 100644 --- a/cmd/gobind/gen.go +++ b/cmd/gobind/gen.go @@ -69,12 +69,12 @@ func genPkg(lang string, p *types.Package, astFiles []*ast.File, allPkg []*types closer() } buf.Reset() - w, closer = writer(filepath.Join("src", "gobind", pname+"_android.c")) + w, closer = writer(filepath.Join("src", "gobind", pname+"_java.c")) processErr(g.GenC()) io.Copy(w, &buf) closer() buf.Reset() - w, closer = writer(filepath.Join("src", "gobind", pname+"_android.h")) + w, closer = writer(filepath.Join("src", "gobind", pname+"_java.h")) processErr(g.GenH()) io.Copy(w, &buf) closer() @@ -86,7 +86,7 @@ func genPkg(lang string, p *types.Package, astFiles []*ast.File, allPkg []*types return } repo := filepath.Clean(filepath.Join(dir, "..")) // golang.org/x/mobile directory. - for _, javaFile := range []string{"Seq.java"} { + for _, javaFile := range []string{"Seq.java", "NativeUtils.java"} { src := filepath.Join(repo, "bind/java/"+javaFile) in, err := os.Open(src) if err != nil { @@ -110,9 +110,9 @@ func genPkg(lang string, p *types.Package, astFiles []*ast.File, allPkg []*types errorf("unable to import bind/java: %v", err) return } - copyFile(filepath.Join("src", "gobind", "seq_android.c"), filepath.Join(javaDir, "seq_android.c.support")) - copyFile(filepath.Join("src", "gobind", "seq_android.go"), filepath.Join(javaDir, "seq_android.go.support")) - copyFile(filepath.Join("src", "gobind", "seq_android.h"), filepath.Join(javaDir, "seq_android.h")) + copyFile(filepath.Join("src", "gobind", "seq_java.c"), filepath.Join(javaDir, "seq_android.c.support")) + copyFile(filepath.Join("src", "gobind", "seq_java.go"), filepath.Join(javaDir, "seq_android.go.support")) + copyFile(filepath.Join("src", "gobind", "seq_java.h"), filepath.Join(javaDir, "seq_android.h")) } case "go": w, closer := writer(filepath.Join("src", "gobind", fname)) @@ -174,10 +174,10 @@ func genPkg(lang string, p *types.Package, astFiles []*ast.File, allPkg []*types func genPkgH(w io.Writer, pname string) { fmt.Fprintf(w, `// Code generated by gobind. DO NOT EDIT. -#ifdef __GOBIND_ANDROID__ -#include "%[1]s_android.h" +#ifdef __GOBIND_JAVA__ +#include "%[1]s_java.h" #endif -#ifdef __GOBIND_DARWIN__ +#if defined(__GOBIND_DARWIN__) && !defined(__GOBIND_JAVA__) #include "%[1]s_darwin.h" #endif`, pname) } @@ -275,7 +275,7 @@ func genJavaPackages(dir string, classes []*java.Class, embedders []importers.St } buf.Reset() cg.GenGo() - if err := ioutil.WriteFile(filepath.Join(goBase, "classes_android.go"), buf.Bytes(), 0600); err != nil { + if err := ioutil.WriteFile(filepath.Join(goBase, "classes_java.go"), buf.Bytes(), 0600); err != nil { return err } buf.Reset() @@ -285,7 +285,7 @@ func genJavaPackages(dir string, classes []*java.Class, embedders []importers.St } buf.Reset() cg.GenC() - if err := ioutil.WriteFile(filepath.Join(goBase, "classes_android.c"), buf.Bytes(), 0600); err != nil { + if err := ioutil.WriteFile(filepath.Join(goBase, "classes_java.c"), buf.Bytes(), 0600); err != nil { return err } return nil diff --git a/cmd/gomobile/bind.go b/cmd/gomobile/bind.go index 11b406256..f4c7b5832 100644 --- a/cmd/gomobile/bind.go +++ b/cmd/gomobile/bind.go @@ -25,7 +25,7 @@ import ( var cmdBind = &command{ run: runBind, Name: "bind", - Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-bootclasspath ] [-classpath ] [-o output] [build flags] [package]", + Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "|java] [-bootclasspath ] [-classpath ] [-o output] [build flags] [package]", Short: "build a library for Android and iOS", Long: ` Bind generates language bindings for the package named by the import @@ -133,6 +133,8 @@ func runBind(cmd *command) error { return fmt.Errorf("-target=%q requires Xcode", buildTarget) } return goAppleBind(gobind, pkgs, targets) + case isJavaPlatform(targets[0].platform): + return goJavaBind(gobind, pkgs, targets) default: return fmt.Errorf(`invalid -target=%q`, buildTarget) } diff --git a/cmd/gomobile/bind_androidapp.go b/cmd/gomobile/bind_androidapp.go index f8fcabe1b..c8d6b8795 100644 --- a/cmd/gomobile/bind_androidapp.go +++ b/cmd/gomobile/bind_androidapp.go @@ -162,7 +162,7 @@ func buildAAR(srcDir, androidDir string, pkgs []*packages.Package, targets []tar if err != nil { return err } - if err := buildJar(w, srcDir); err != nil { + if err := buildAndroidJar(w, srcDir); err != nil { return err } @@ -249,7 +249,7 @@ const ( minAndroidAPI = 16 ) -func buildJar(w io.Writer, srcDir string) error { +func buildAndroidJar(w io.Writer, srcDir string) error { var srcFiles []string if buildN { srcFiles = []string{"*.java"} diff --git a/cmd/gomobile/bind_javaapp.go b/cmd/gomobile/bind_javaapp.go new file mode 100644 index 000000000..5464f07df --- /dev/null +++ b/cmd/gomobile/bind_javaapp.go @@ -0,0 +1,191 @@ +// Copyright 2015 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" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "golang.org/x/sync/errgroup" + "golang.org/x/tools/go/packages" +) + +var osname = runtime.GOOS + +func goJavaBind(gobind string, pkgs []*packages.Package, targets []targetInfo) error { + // Run gobind to generate the bindings + cmd := exec.Command( + gobind, + "-lang=go,java", + "-outdir="+tmpdir, + ) + if len(buildTags) > 0 { + cmd.Args = append(cmd.Args, "-tags="+strings.Join(buildTags, ",")) + } + if bindJavaPkg != "" { + cmd.Args = append(cmd.Args, "-javapkg="+bindJavaPkg) + } + if bindClasspath != "" { + cmd.Args = append(cmd.Args, "-classpath="+bindClasspath) + } + if bindBootClasspath != "" { + cmd.Args = append(cmd.Args, "-bootclasspath="+bindBootClasspath) + } + for _, p := range pkgs { + cmd.Args = append(cmd.Args, p.PkgPath) + } + if err := runCmd(cmd); err != nil { + return err + } + + outputDir := filepath.Join(tmpdir, "java") + + // Generate binding code and java source code only when processing the first package. + var wg errgroup.Group + for _, t := range targets { + t := t + wg.Go(func() error { + return buildJavaSO(outputDir, t.arch) + }) + } + if err := wg.Wait(); err != nil { + return err + } + + w, err := os.Create(buildO) + if err != nil { + return err + } + jsrc := filepath.Join(tmpdir, "java") + if err := buildJavaJar(w, jsrc); err != nil { + return err + } + _ = os.RemoveAll(filepath.Join(outputDir, "src", "main", "jniLibs")) + return buildSrcJar(jsrc) +} + +func buildJavaJar(w io.Writer, srcDir string) error { + var srcFiles []string + if buildN { + srcFiles = []string{"*.java"} + } else { + err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if filepath.Ext(path) == ".java" { + srcFiles = append(srcFiles, filepath.Join(".", path[len(srcDir):])) + } + return nil + }) + if err != nil { + return err + } + } + + dst := filepath.Join(tmpdir, "javac-output") + if !buildN { + if err := os.MkdirAll(dst, 0700); err != nil { + return err + } + } + + cp := exec.Command("cp", "-r", filepath.Join(tmpdir, "java", "src", "main", "jniLibs"), dst) + if err := runCmd(cp); err != nil { + return err + } + + args := []string{ + "-d", dst, + "-source", javacTargetVer, + "-target", javacTargetVer, + } + if bindClasspath != "" { + args = append(args, "-classpath", bindClasspath) + } + + args = append(args, srcFiles...) + + javac := exec.Command("javac", args...) + javac.Dir = srcDir + if err := runCmd(javac); err != nil { + return err + } + + if buildX { + printcmd("jar c -C %s .", dst) + } + return writeJar(w, dst) +} + +// buildJavaSO generates a libgojni.so file to outputDir (regardless of OS library file extension). +// buildJavaSO is concurrent-safe. +func buildJavaSO(outputDir string, arch string) error { + // Copy the environment variables to make this function concurrent-safe. + env := make([]string, len(javaEnv[arch])) + copy(env, javaEnv[arch]) + + // Add the generated packages to GOPATH for reverse bindings. + gopath := fmt.Sprintf("GOPATH=%s%c%s", tmpdir, filepath.ListSeparator, goEnv("GOPATH")) + env = append(env, gopath) + + modulesUsed, err := areGoModulesUsed() + if err != nil { + return err + } + + srcDir := filepath.Join(tmpdir, "src") + + if modulesUsed { + // Copy the source directory for each architecture for concurrent building. + newSrcDir := filepath.Join(tmpdir, "src-"+osname+"-"+arch) + if !buildN { + if err := doCopyAll(newSrcDir, srcDir); err != nil { + return err + } + } + srcDir = newSrcDir + + if err := writeGoMod(srcDir, "java", arch); err != nil { + return err + } + + // Run `go mod tidy` to force to create go.sum. + // Without go.sum, `go build` fails as of Go 1.16. + if err := goModTidyAt(srcDir, env); err != nil { + return err + } + } + + // Javaify the arch descriptors + if arch == "arm64" { + if osname == "linux" { + env = append(env, "CC=aarch64-linux-gnu-gcc") + // env = append(env, "CC=clang") + } + } + if arch == "amd64" { + if osname == "linux" { + env = append(env, "CC=x86_64-linux-gnu-gcc") + // env = append(env, "CC=clang") + } + } + if err := goBuildAt( + srcDir, + "./gobind", + env, + "-buildmode=c-shared", + "-o="+filepath.Join(outputDir, "src", "main", "jniLibs", arch, "libgojni.so"), + ); err != nil { + return err + } + + return nil +} diff --git a/cmd/gomobile/build.go b/cmd/gomobile/build.go index c9483434d..fc4cf2ae8 100644 --- a/cmd/gomobile/build.go +++ b/cmd/gomobile/build.go @@ -394,7 +394,7 @@ func parseBuildTarget(buildTarget string) ([]targetInfo, error) { } } - var isAndroid, isApple bool + var isAndroid, isApple, isJava bool for _, target := range strings.Split(buildTarget, ",") { tuple := strings.SplitN(target, "/", 2) platform := tuple[0] @@ -404,11 +404,13 @@ func parseBuildTarget(buildTarget string) ([]targetInfo, error) { isAndroid = true } else if isApplePlatform(platform) { isApple = true + } else if isJavaPlatform(platform) { + isJava = true } else { return nil, fmt.Errorf("unsupported platform: %q", platform) } - if isAndroid && isApple { - return nil, fmt.Errorf(`cannot mix android and Apple platforms`) + if isAndroid && isApple && isJava { + return nil, fmt.Errorf(`cannot mix android and Apple and Java platforms`) } if hasArch { diff --git a/cmd/gomobile/env.go b/cmd/gomobile/env.go index 43f24b99d..9c52c82ef 100644 --- a/cmd/gomobile/env.go +++ b/cmd/gomobile/env.go @@ -22,12 +22,17 @@ var ( androidEnv map[string][]string // android arch -> []string appleEnv map[string][]string appleNM string + javaEnv map[string][]string ) func isAndroidPlatform(platform string) bool { return platform == "android" } +func isJavaPlatform(platform string) bool { + return platform == "java" +} + func isApplePlatform(platform string) bool { return contains(applePlatforms, platform) } @@ -44,6 +49,8 @@ func platformArchs(platform string) []string { return []string{"arm64", "amd64"} case "android": return []string{"arm", "arm64", "386", "amd64"} + case "java": + return []string{"arm64", "amd64"} default: panic(fmt.Sprintf("unexpected platform: %s", platform)) } @@ -56,6 +63,8 @@ func isSupportedArch(platform, arch string) bool { // platformOS returns the correct GOOS value for platform. func platformOS(platform string) string { switch platform { + case "java": + return runtime.GOOS case "android": return "android" case "ios", "iossimulator": @@ -73,8 +82,10 @@ func platformOS(platform string) string { func platformTags(platform string) []string { switch platform { + case "java": + return []string{"java"} case "android": - return []string{"android"} + return []string{"android", "java"} case "ios", "iossimulator": return []string{"ios"} case "macos": @@ -185,6 +196,7 @@ func envInit() (err error) { "CC=" + clang, "CXX=" + clangpp, "CGO_ENABLED=1", + "GOFLAGS=" + "-tags=" + strings.Join(platformTags("android"), ","), } if arch == "arm" { androidEnv[arch] = append(androidEnv[arch], "GOARM=7") @@ -192,6 +204,29 @@ func envInit() (err error) { } } + javaHome := os.Getenv("JAVA_HOME") + if javaHome == "" { + javac, err := exec.LookPath("javac") + if err != nil { + return err + } + javac, err = filepath.EvalSymlinks(javac) + if err != nil { + return err + } + javaHome = strings.TrimSuffix(javac, "/bin/javac") + } + + javaEnv = make(map[string][]string) + for _, arch := range platformArchs("java") { + javaEnv[arch] = []string{ + "GOARCH=" + arch, + "CGO_ENABLED=1", + "GOFLAGS=" + "-tags=" + strings.Join(platformTags("java"), ","), + "CGO_CFLAGS=\"-I" + javaHome + "/include/\" \"-I" + javaHome + "/include/" + osname + "/\"", + } + } + if !xcodeAvailable() { return nil }