001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package hivemall.xgboost.utils;
020
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.lang.reflect.Field;
028import java.util.UUID;
029
030import javax.annotation.Nonnull;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034
035public final class NativeLibLoader {
036    private static final Log logger = LogFactory.getLog(NativeLibLoader.class);
037
038    private static final String keyUserDefinedLib = "hivemall.xgboost.lib";
039    private static final String libPath = "/lib/";
040
041    private static boolean initialized = false;
042
043    // Try to load a native library if it exists
044    public static synchronized void initXGBoost() {
045        if (!initialized) {
046            // Since a user-defined native library has a top priority,
047            // we first check if it is defined or not.
048            final String userDefinedLib = System.getProperty(keyUserDefinedLib);
049            if (userDefinedLib == null) {
050                tryLoadNativeLibFromResource("xgboost4j");
051            } else {
052                tryLoadNativeLib(userDefinedLib);
053            }
054            initialized = true;
055        }
056    }
057
058    private static boolean hasResource(String path) {
059        return NativeLibLoader.class.getResource(path) != null;
060    }
061
062    @Nonnull
063    private static String getOSName() {
064        return System.getProperty("os.name");
065    }
066
067    private static void tryLoadNativeLibFromResource(final String libName) {
068        // Resolve the library file name with a suffix (e.g., dll, .so, etc.)
069        String resolvedLibName = System.mapLibraryName(libName);
070
071        if (!hasResource(libPath + resolvedLibName)) {
072            if (!getOSName().equals("Mac")) {
073                return;
074            }
075            // Fix for openjdk7 for Mac
076            // A detail of this workaround can be found in https://github.com/xerial/snappy-java/issues/6
077            resolvedLibName = "lib" + libName + ".jnilib";
078            if (hasResource(libPath + resolvedLibName)) {
079                return;
080            }
081        }
082        try {
083            File tempFile = createTempFileFromResource(resolvedLibName,
084                NativeLibLoader.class.getResourceAsStream(libPath + resolvedLibName));
085            logger.info("Copyed the native library in JAR as " + tempFile.getAbsolutePath());
086            addLibraryPath(tempFile.getParent());
087        } catch (Exception e) {
088            // Simply ignore it here
089            logger.info(e.getMessage());
090        }
091    }
092
093    private static void tryLoadNativeLib(final String userDefinedLib) {
094        final File userDefinedLibFile = new File(userDefinedLib);
095        if (!userDefinedLibFile.exists()) {
096            logger.warn(userDefinedLib + " not found");
097        } else {
098            try {
099                File tempFile = createTempFileFromResource(userDefinedLibFile.getName(),
100                    new FileInputStream(userDefinedLibFile.getAbsolutePath()));
101                logger.info(
102                    "Copyed the user-defined native library as " + tempFile.getAbsolutePath());
103                addLibraryPath(tempFile.getParent());
104            } catch (Exception e) {
105                // Simply ignore it here
106                logger.warn(e.getMessage());
107            }
108        }
109    }
110
111    @Nonnull
112    private static String getPrefix(@Nonnull String fileName) {
113        int point = fileName.lastIndexOf(".");
114        if (point != -1) {
115            return fileName.substring(0, point);
116        }
117        return fileName;
118    }
119
120    /**
121     * Create a temp file that copies the resource from current JAR archive.
122     *
123     * @param libName Library name with a suffix
124     * @param is Input stream to the native library
125     * @return The created temp file
126     * @throws IOException
127     * @throws IllegalArgumentException
128     */
129    static File createTempFileFromResource(String libName, InputStream is)
130            throws IOException, IllegalArgumentException {
131        // Create a temporary folder with a random number for the native lib
132        final String uuid = UUID.randomUUID().toString();
133        final File tempFolder = new File(System.getProperty("java.io.tmpdir"),
134            String.format("%s-%s", getPrefix(libName), uuid));
135        if (!tempFolder.exists()) {
136            boolean created = tempFolder.mkdirs();
137            if (!created) {
138                throw new IOException("Failed to create a temporary folder for the native lib");
139            }
140        }
141
142        // Prepare buffer for data copying
143        final byte[] buffer = new byte[8192];
144        int readBytes;
145
146        // Open output stream and copy the native library into the temporary one
147        File extractedLibFile = new File(tempFolder.getAbsolutePath(), libName);
148        final OutputStream os = new FileOutputStream(extractedLibFile);
149        try {
150            while ((readBytes = is.read(buffer)) != -1) {
151                os.write(buffer, 0, readBytes);
152            }
153        } finally {
154            // If read/write fails, close streams safely before throwing an exception
155            os.close();
156            is.close();
157        }
158        return extractedLibFile;
159    }
160
161    /**
162     * Add libPath to java.library.path, then native library in libPath would be load properly.
163     *
164     * @param libPath library path
165     * @throws IOException exception
166     */
167    private static void addLibraryPath(String libPath) throws IOException {
168        try {
169            final Field field = ClassLoader.class.getDeclaredField("usr_paths");
170            field.setAccessible(true);
171            final String[] paths = (String[]) field.get(null);
172            for (String path : paths) {
173                if (libPath.equals(path)) {
174                    return;
175                }
176            }
177            final String[] tmp = new String[paths.length + 1];
178            System.arraycopy(paths, 0, tmp, 0, paths.length);
179            tmp[paths.length] = libPath;
180            field.set(null, tmp);
181        } catch (IllegalAccessException e) {
182            logger.error(e.getMessage());
183            throw new IOException("Failed to get permissions to set library path");
184        } catch (NoSuchFieldException e) {
185            logger.error(e.getMessage());
186            throw new IOException("Failed to get field handle to set library path");
187        }
188    }
189
190}