com.twitter.common.runtime.NativeLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.twitter.common.runtime.NativeLoader.java

Source

// =================================================================================================
// Copyright 2012 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or at:
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =================================================================================================

package com.twitter.common.runtime;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;

import com.google.common.base.CharMatcher;
import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.common.io.Resources;

import com.twitter.common.base.Function;
import com.twitter.common.base.MorePreconditions;
import com.twitter.common.io.FileUtils;

/**
 * Provides a facility for extracting and optionally loading native libraries from classpath
 * resources.
 *
 * <p>NativeLoader can be used in 2 modes, possibly intermixed:
 * <ol>
 *   <li>To establish a path to adjoin to the native library path on the system at hand.</li>
 *   <li>To load contained jni libraries.</li>
 * </ol>
 *
 * <p>In the 1st mode the transitive set of non-core libraries needed by a java jni application
 * would be included as native resources inside jars, extracted to a directory and adjoined to the
 * library path.  On linux this would might look like:
 * <pre>
 *   #!/bin/bash
 *
 *   MY_NATIVE_LIBS=$(mktemp -d)
 *   trap "rm -r $MY_NATIVE_LIBS" EXIT
 *   LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MY_NATIVE_LIBS \
 *     java -Dnativeloader.library.path=$MY_NATIVE_LIBS \
 *          -cp ...
 * </pre>
 * </p>
 *
 * <p>In the second mode, rather than using System.loadLibrary([libname]) and adjusting
 * java.library.path so that the jvm can find your jni libs, you instead let NativeLoader load your
 * jni libraries from the extracted resources directly.  This loading is triggered by special syntax
 * described below.
 * </p>
 *
 * <p>NativeLoader relies on a special classpath manifest resource to describe native libraries in
 * the classpath.  The resource path is {@code META-INF/native.mf} and the contents is a
 * line-oriented listing of classpath native library resources.  Each library line can begin with a
 * single optional {@code '*'} indicating the library should be {@link System#load(String) loaded}.
 * This is followed by the classpath resource name of the native library and optionally followed by
 * one or more whitespace separated paths to link the resource to when it is extracted onto the
 * filesystem.
 *
 * <p>For example:
 * <pre>
 *   # We extract the manifest itself too.  We don't need to have this entry but any resource can be
 *   # extracted even if its not actually a native library.
 *   META-INF/native.mf
 *
 *   # We extract libopencv_core.2.4.2.dylib from the classpath but also create a symlink for
 *   # libopencv_core.2.4.dylib in the extraction directory.
 *   libopencv_core.2.4.2.dylib    libopencv_core.2.4.dylib
 *
 *   # The next 2 libraries are loaded and java.library.path need not be modified.
 *   *libjnimesos.dylib
 *   *mecab.dylib
 * </pre>
 * </p>
 *
 * <p>Extracts to:
 * <pre>
 *   [lib dir]/META-INF/native.mf
 *   [lib dir]/libopencv_core.2.4.2.dylib
 *   [lib dir]/libopencv_core.2.4.dylib -> libopencv_core.2.4.2.dylib
 *   [lib dir]/libjnimesos.dylib
 *   [lib dir]/mecab.dylib
 * </pre>
 * </p>
 *
 * Note that although order does not matter for parsing it is respected when loading libraries
 * marked with the load ({@code '*'}) directive.  In the example above 1st all extraction is done
 * and then libjnimesos.dylib is loaded followed by mecab.dylib.
 */
public final class NativeLoader {
    private static final Logger LOG = Logger.getLogger(NativeLoader.class.getName());

    private static class Global {
        private static File getLibPath() {
            String libPath = System.getProperty("nativeloader.library.path",
                    System.getenv("NATIVELOADER_LIBRARY_PATH"));
            if (libPath != null) {
                return new File(libPath);
            } else {
                return FileUtils.createTempDir();
            }
        }

        private static boolean getDeleteExtractedOnExit() {
            return Boolean.getBoolean("nativeloader.deleteonexit");
        }

        static final NativeLoader LOADER = new NativeLoader(getLibPath(), getDeleteExtractedOnExit());
    }

    /**
     * Extracts any registered native libraries from the classpath and loads those marked for load.
     *
     * <p>Extraction behavior can be controlled through a combination of environment variables and
     * system properties:
     * <ul>
     *   <li>NATIVELOADER_LIBRARY_PATH: An environment variable containing the path to extract to.
     *   If not present a new random temp dir will be used.</li>
     *   <li>nativeloader.library.path: A system property containing the path to extract to.  If not
     *   present then NATIVELOADER_LIBRARY_PATH is used.</li>
     *   <li>nativeloader.deleteonexit: A system property that can be set to 'false' to turn off the
     *   default delete-on-exit of extracted resources.</li>
     * </ul>
     * </p>
     *
     * <p>A common use for this static method would be as a replacement for a typical:
     * <pre>
     *   class MyNativeBridge {
     *     static {
     *       System.loadLibrary('mesos');
     *     }
     *   }
     * </pre>
     *
     * With:
     * <pre>
     *   class MyNativeBridge {
     *     static {
     *       NativeLoader.loadLibs();
     *     }
     *   }
     * </pre>
     * </p>
     * @return The native resources found on the classpath and extracted.
     */
    public static ImmutableList<NativeResource> loadLibs() {
        return Global.LOADER.load();
    }

    /**
     * Indicates an error loading a native library.
     */
    public static class NativeLoadError extends Error {
        public NativeLoadError(Throwable cause) {
            super(cause);
        }
    }

    private final File libPath;
    private final boolean deleteExtractedOnExit;

    /**
     * Creates a native loader that extracts any registered native libraries to the designated
     * {@code libPath}.
     *
     * @param libPath The path to extract native library resources to.
     * @param deleteExtractedOnExit If {@code true}, extracted libraries will be deleted when the jvm
     *     exits.
     */
    public NativeLoader(File libPath, boolean deleteExtractedOnExit) {
        Preconditions.checkNotNull(libPath);
        Preconditions.checkArgument(libPath.exists() || libPath.mkdirs(),
                "%s does not exist and failed to create it", libPath);
        Preconditions.checkArgument(libPath.canWrite(), "%s exists but cannot write to it", libPath);

        this.libPath = libPath;
        this.deleteExtractedOnExit = deleteExtractedOnExit;
    }

    private final Supplier<ImmutableList<NativeResource>> nativeResources = Suppliers
            .memoize(new Supplier<ImmutableList<NativeResource>>() {
                @Override
                public ImmutableList<NativeResource> get() {
                    try {
                        return extractLibs();
                    } catch (IOException e) {
                        throw new NativeLoadError(e);
                    }
                }
            });

    /**
     * Extracts any registered native libraries from the classpath, creates links (or copies) as
     * needed and loads those libraries marked for load.  This method is idempotent and will only do
     * extraction and loading on the first call.  All subsequent calls will just return the list
     * of already loaded native resources.
     *
     * @return The native resources that were loaded.
     */
    public ImmutableList<NativeResource> load() {
        return nativeResources.get();
    }

    ImmutableList<NativeResource> extractLibs() throws IOException {
        // Extract all native libs before loading them - libs may interdepend and need sibling loose
        // on the path to resolve fully.

        ImmutableList.Builder<NativeResource> resourceBuilder = ImmutableList.builder();
        for (NativeResource nativeResource : findNativeResources()) {
            nativeResource.extract();
            resourceBuilder.add(nativeResource);
        }
        ImmutableList<NativeResource> resources = resourceBuilder.build();

        for (NativeResource nativeResource : resources) {
            nativeResource.maybeLoad();
        }

        return resources;
    }

    private Iterable<NativeResource> findNativeResources() throws IOException {
        Enumeration<URL> resourcesEnumeration = getClass().getClassLoader().getResources("META-INF/native.mf");

        Set<NativeResource> resources = Sets.newLinkedHashSet();
        while (resourcesEnumeration.hasMoreElements()) {
            URL manifestUrl = resourcesEnumeration.nextElement();
            for (String line : Resources.readLines(manifestUrl, Charsets.UTF_8)) {
                String normalizedLine = line.trim();
                if (!normalizedLine.startsWith("#")) {
                    NativeResource nativeResource = NativeResource.parse(libPath, deleteExtractedOnExit,
                            normalizedLine);
                    if (!resources.add(nativeResource)) {
                        throw new IllegalStateException(
                                "Already detected a native resource for " + normalizedLine + " in " + manifestUrl);
                    }
                }
            }
        }
        LOG.info("Found native resources: " + resources);
        return resources;
    }

    /**
     * Describes a native resource that extracts to a library path.
     */
    public static final class NativeResource {

        static NativeResource parse(File libPath, boolean deleteOnExit, String normalizedLine) {
            if (normalizedLine.startsWith("*")) {
                return new NativeResource(libPath, normalizedLine.substring(1), true, deleteOnExit);
            } else {
                return new NativeResource(libPath, normalizedLine, false, deleteOnExit);
            }
        }

        private final File file;
        private Set<File> links;
        private final String name;
        private final boolean loadable;
        private final boolean deleteOnExit;

        private NativeResource(final File basedir, String names, boolean loadable, boolean deleteOnExit) {

            Iterable<String> nameAndLinks = Lists
                    .newArrayList(Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings().split(names));
            MorePreconditions.checkNotBlank(nameAndLinks);

            Iterator<String> paths = Iterators.consumingIterator(nameAndLinks.iterator());

            name = paths.next();

            Function<String, File> createPath = new Function<String, File>() {
                @Override
                public File apply(String item) {
                    return new File(basedir, item);
                }
            };
            file = createPath.apply(name);
            links = ImmutableSet.copyOf(Iterators.transform(paths, createPath));

            this.loadable = loadable;
            this.deleteOnExit = deleteOnExit;
        }

        void extract() throws IOException {
            if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
                throw new IOException("Failed to create parent dir for " + this);
            }

            LOG.info("Extracting " + this);
            Files.copy(Resources.newInputStreamSupplier(Resources.getResource(name)), file);
            if (deleteOnExit) {
                file.deleteOnExit();
            }

            // TODO(John Sirois): really just link - java 7 supports this.
            for (File link : links) {
                LOG.info(String.format("Linking %s -> %s", link, file));
                Files.copy(file, link);
                if (deleteOnExit) {
                    link.deleteOnExit();
                }
            }
        }

        void maybeLoad() {
            if (loadable) {
                LOG.info("Loading " + this);
                System.load(file.getPath());
            }
        }

        /**
         * Returns the file the native library is extracted to.
         */
        public File getFile() {
            return file;
        }

        /**
         * Returns the resource name of the classpath embedded native resource.
         */
        public String getName() {
            return name;
        }

        /**
         * Returns {@code true} if the native resource is loadable via {@link System#load(String)}.
         */
        public boolean isLoadable() {
            return loadable;
        }

        @Override
        public String toString() {
            return Objects.toStringHelper(this).add("name", name).add("file", file).add("loadable", loadable)
                    .add("links", links).toString();
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof NativeResource)) {
                return false;
            }

            NativeResource that = (NativeResource) o;
            return Objects.equal(file, that.file) && Objects.equal(name, that.name)
                    && Objects.equal(loadable, that.loadable);
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(file, name, loadable);
        }
    }
}