Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: Adaptable NativeLibraryLoader #32

Open
BjoernAkAManf opened this issue May 3, 2022 · 7 comments
Open

Discussion: Adaptable NativeLibraryLoader #32

BjoernAkAManf opened this issue May 3, 2022 · 7 comments

Comments

@BjoernAkAManf
Copy link
Contributor

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
Copy link
Owner

kawamuray commented May 8, 2022

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
Copy link
Contributor Author

BjoernAkAManf commented May 8, 2022

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
Copy link
Owner

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
Copy link
Contributor Author

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
Copy link
Owner

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
Copy link
Contributor Author

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
Copy link
Owner

Ok, so in summary:

In development phase:

  • Pull this repository and build manually just by ./gradlew build <= This is also possible

In deployment phase:

  • Pulls this repository and build <= Possible already
  • Sign the binary <= will be done by customizing (extra step) build, so not a matter of this repository
  • Provision the binary to a filesystem that will be read-only at runtime <= not a matter of this repository

And at runtime:

  • JVM running wasmtime-java loads the native library from arbitrary location <= this is possible with java.library.path but you want an alternative way

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants