kawamuray / wasmtime-java

Java or JVM-language binding for Wasmtime
Apache License 2.0
128 stars 29 forks source link

Discussion: Adaptable NativeLibraryLoader #32

Open BjoernAkAManf opened 2 years ago

BjoernAkAManf commented 2 years ago

Hi,

as a User of this Library i want to be able to control access of IO. In particular i want to be able to specify the path the native library is beeing written to.

For example i attached the following Class "Native" that emulates the behavior i want to implement. Unfortunately this requires alot of copy-paste and hackish workarounds.

I would propose extracting the necessary functionality into atleast two parts:

  1. General Utility Class providing init() and load() delegating to implementation
  2. Strategy API -> Allows to implement a System.load() strategy

By default i suggest implementing the following Strategies:

  1. tryLoadFromLibraryPath -> Same as the corresponding method
  2. loadFromTempFile -> Same as current Implementation provided by libraryPath()

Order is provided by each Strategy. As such tryLoadFromLibraryPath would return -1 and loadFromTempfile 1. Other Implementations can then use 0 easily. ServiceLoader would allow to reduce implementation.

The API should provide access to atleast fileName and extension in order to allow writing correctly.

I did not yet start working on a Pull Request, because i feel like this RfC is disruptive to the current implementation and as such I feel @kawamuray you as a Maintainer should first give your Opinion.

Alternatively one could use an Environment Variable, but i feel that is too restrictive for other use cases. Especially when one wants to validate DLL using an out of band signing process.

import io.github.kawamuray.wasmtime.NativeLibraryLoader;
import lombok.AllArgsConstructor;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import java.util.Properties;

/**
 * This is a copy of {@link NativeLibraryLoader} that is needed to extract to a well known location instead of a temp file.
 */
@Slf4j
@UtilityClass
public final class Native {
    private static final String SANDBOX_NATIVE_OVERRIDE = "SANDBOX_NATIVE_OVERRIDE";
    private static final String LOCATION = ".native";
    private boolean loaded = false;

    public void load() {
        if (loaded) {
            return;
        }

        if (Native.isAutoLoadDisabled()) {
            log.error("Please set Environment Variable {} to a Value", DISABLE_AUTO_LOAD_ENV);
            System.exit(1);
        }

        try {
            final var nativeLibraryDir = Paths.get(LOCATION);
            if (!Files.exists(nativeLibraryDir)) {
                Files.createDirectory(nativeLibraryDir);
            }
            final var libraryPath = libraryPath(nativeLibraryDir);
            log.debug("Loading Wasmtime JNI library from {}", libraryPath);
            System.load(libraryPath);
            loaded = true;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private boolean isAutoLoadDisabled() {
        return System.getenv(DISABLE_AUTO_LOAD_ENV) == null;
    }

    private boolean isOverridingNative() {
        return System.getenv(SANDBOX_NATIVE_OVERRIDE) != null;
    }

    // Copied and changed
    private static String libraryPath(final Path root) throws IOException {
        final var platform = detectPlatform();
        String version = libVersion();
        String ext = platform.ext;
        String fileName = platform.prefix + NATIVE_LIBRARY_NAME + '_' + version + '_' + platform.classifier;
        final var name = fileName + ext;
        final var p = root.resolve(name);
        final var ovr = isOverridingNative();
        if (ovr) {
            log.warn("Overriding existing stuff yadda yaadda");
        }
        if (ovr || !Files.exists(p)) {
            try (final var in = NativeLibraryLoader.class.getResourceAsStream('/' + name)) {
                // Added to copied struct
                Objects.requireNonNull(in, "Could not find Library");
                final var options = ovr
                    ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING}
                    : new CopyOption[]{};
                Files.copy(in, p, options);
            }
        }
        return p.toRealPath().toAbsolutePath().toString();
    }

    // Rest is copied from Version 0.9.0
    private static final String DISABLE_AUTO_LOAD_ENV = "WASMTIME_JNI_LOAD_DISABLED";
    private static final String NATIVE_LIBRARY_NAME = "wasmtime_jni";
    private static final String META_PROPS_FILE = "wasmtime-java-meta.properties";

    private static final String JNI_LIB_VERSION_PROP = "jnilib.version";

    @AllArgsConstructor
    private enum Platform {
        LINUX("linux", "lib", ".so"),
        MACOS("macos", "lib", ".dylib"),
        WINDOWS("windows", "", ".dll");
        final String classifier;
        final String prefix;
        final String ext;

    }

    private static Platform detectPlatform() {
        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("linux")) {
            return Platform.LINUX;
        }
        if (os.contains("mac os") || os.contains("darwin")) {
            return Platform.MACOS;
        }
        if (os.toLowerCase().contains("windows")) {
            return Platform.WINDOWS;
        }
        throw new RuntimeException("platform not supported: " + os);
    }

    private static String libVersion() throws IOException {
        final Properties props;
        try (InputStream in = NativeLibraryLoader.class.getResourceAsStream('/' + META_PROPS_FILE)) {
            props = new Properties();
            props.load(in);
        }
        return props.getProperty(JNI_LIB_VERSION_PROP);
    }
}
kawamuray commented 2 years ago

Hi @BjoernAkAManf , thanks for your proposal!

Leaving the discussion of the API for a PR, I think providing a way to custom behavior around native library loading itself is fine as long as we have some expected use-cases. What is your concrete use case for the custom library loading?

BjoernAkAManf commented 2 years ago

A couple of things actually (at least visionary):

  1. I would prefer writing the library to disk once, and not replacing it whenever. This is a key requirement as I'm planning to run my application within an environment, where writing to disk by the application is not permitted.
  2. I would like to be able to define the location of the library myself, so that it is saved relative to the application in a .native folder.
  3. I would like to be able to verify and sign the native library both in the application and externally. In particular securing the software supply chain through self hosted, independently verified builds.

I'll try to prepare a Pull Request once i get the time.

kawamuray commented 2 years ago

I see, these requirements are reasonable I think.

So basically you want to put a custom (signed) native library to the location (application-local directory) through the build process and let the JVM to load native library from there without extracting an included build out of wasmtime-java jar?

If so, would it be doable just by using java.library.path system property? JVM searches native library from the directory specified to the property, so by putting it into the .native directory for example, you can just java ... -Djava.library.path=/path/to/.native then tryLoadFromLibraryPath loads from there without writing any to the disks.

BjoernAkAManf commented 2 years ago

Yes, agreed, that this is somewhat possible right now. However i disagree with the Solution:

java.library.path is read-only after JVM start though. Workarounds seem way to hacky (e.g. http://web.archive.org/web/20210614015640/http://fahdshariff.blogspot.com/2011/08/changing-java-library-path-at-runtime.html ). As such i would have either to:

  1. Write a Wrapper to set the parameter on startup
  2. Document the behavior and force users to set the parameter.
  3. Use the hacky workaround

Both of which seem quite counter-intuitive for a Cross Platform Language like java (albeit i agree this is somewhat of a standard). However all of those seem like a bad choice, if a "simple" Class (see my initial comment) can fix that. As such i still think my use-case is hardly covered by existing alternatives.

Also i think being able to initialize the application on any supported operating system "as-is" is quite the desireable Dev-Experience and i would therefore prefer being able to write the currently shipped binary to disk.

kawamuray commented 2 years ago

i would therefore prefer being able to write the currently shipped binary to disk.

So you want wasmtime-java to extract the pre-built jar out of it and use still, but you want control where it's stored temporary? I'm little bit confused because, the above requirement indeed fulfils use-case 2, (application local file creation) but doesn't sounds to help use-case 1 and 3 because in case of 1, the running jvm process can't write to disk (so I presume deployer builds and places native library manually), and for 3 you need to run your own build process to sign a build native library I guess?

BjoernAkAManf commented 2 years ago

I'm sorry, i feel like i'm being quite bad at explaining my use-case.

I enjoy being able to just add a maven dependency and "it just works" is great. During development i would want to be able to just download the source code, run maven and "it just works".

During deployment in production however, i would prefer the process to be slightly different with a read-only file system. As i have yet to write that code though, i would assume it would be done through an ansible role. The Native Library would be put there as well. The Signing Process is also still in development, but I'm looking into reproducible builds with a trusted Build Server signing the binary.

As such in development the binary is copied over and in production the binary is expected to be provisioned by external systems. In both cases the location of the binary needs to be predictable.

kawamuray commented 2 years ago

Ok, so in summary:

In development phase:

In deployment phase:

And at runtime:

is this understanding correct? In case, I would still encourage to just go with java.library.path because it's pretty standard for Java libraries, and the reason I put tryLoadFromLibraryPath in prior to prebuilt library extraction is to let users use their own native library through java.library.path.

But if that still doesn't satisfy your need, you can submit a PR with concrete code and we can discuss further. At least i'm not strongly against adding interface to let users inject arbitrary native library loading, as I assume it will be doable with a few simple and stable APIs.