001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.util; 006 007import java.io.File; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.OutputStream; 011import java.nio.charset.StandardCharsets; 012import java.nio.file.Files; 013import java.nio.file.Paths; 014import java.security.DigestInputStream; 015import java.security.MessageDigest; 016import java.security.NoSuchAlgorithmException; 017import java.util.Locale; 018import java.util.Scanner; 019 020/** 021 * Loads a native library at runtime. 022 * 023 * @param <T> The class to load. 024 */ 025public final class RuntimeLoader<T> { 026 private static String defaultExtractionRoot; 027 028 /** 029 * Gets the default extraction root location (~/.wpilib/nativecache). 030 * 031 * @return The default extraction root location. 032 */ 033 public static synchronized String getDefaultExtractionRoot() { 034 if (defaultExtractionRoot != null) { 035 return defaultExtractionRoot; 036 } 037 String home = System.getProperty("user.home"); 038 defaultExtractionRoot = Paths.get(home, ".wpilib", "nativecache").toString(); 039 return defaultExtractionRoot; 040 } 041 042 private final String m_libraryName; 043 private final Class<T> m_loadClass; 044 private final String m_extractionRoot; 045 046 /** 047 * Creates a new library loader. 048 * 049 * @param libraryName Name of library to load. 050 * @param extractionRoot Location from which to load the library. 051 * @param cls Class whose classpath the given library belongs. 052 */ 053 public RuntimeLoader(String libraryName, String extractionRoot, Class<T> cls) { 054 m_libraryName = libraryName; 055 m_loadClass = cls; 056 m_extractionRoot = extractionRoot; 057 } 058 059 /** 060 * Returns a load error message given the information in the provided UnsatisfiedLinkError. 061 * 062 * @param ule UnsatisfiedLinkError object. 063 * @return A load error message. 064 */ 065 private String getLoadErrorMessage(UnsatisfiedLinkError ule) { 066 StringBuilder msg = new StringBuilder(512); 067 msg.append(m_libraryName) 068 .append( 069 " could not be loaded from path or an embedded resource.\n" 070 + "\tattempted to load for platform ") 071 .append(RuntimeDetector.getPlatformPath()) 072 .append("\nLast Load Error: \n") 073 .append(ule.getMessage()) 074 .append('\n'); 075 if (RuntimeDetector.isWindows()) { 076 msg.append( 077 "A common cause of this error is missing the C++ runtime.\n" 078 + "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n"); 079 } 080 return msg.toString(); 081 } 082 083 /** 084 * Loads a native library. 085 * 086 * @throws IOException if the library fails to load 087 */ 088 public void loadLibrary() throws IOException { 089 try { 090 // First, try loading path 091 System.loadLibrary(m_libraryName); 092 } catch (UnsatisfiedLinkError ule) { 093 // Then load the hash from the resources 094 String hashName = RuntimeDetector.getHashLibraryResource(m_libraryName); 095 String resName = RuntimeDetector.getLibraryResource(m_libraryName); 096 try (InputStream hashIs = m_loadClass.getResourceAsStream(hashName)) { 097 if (hashIs == null) { 098 throw new IOException(getLoadErrorMessage(ule)); 099 } 100 try (Scanner scanner = new Scanner(hashIs, StandardCharsets.UTF_8)) { 101 String hash = scanner.nextLine(); 102 File jniLibrary = new File(m_extractionRoot, resName + "." + hash); 103 try { 104 // Try to load from an already extracted hash 105 System.load(jniLibrary.getAbsolutePath()); 106 } catch (UnsatisfiedLinkError ule2) { 107 // If extraction failed, extract 108 try (InputStream resIs = m_loadClass.getResourceAsStream(resName)) { 109 if (resIs == null) { 110 throw new IOException(getLoadErrorMessage(ule)); 111 } 112 113 var parentFile = jniLibrary.getParentFile(); 114 if (parentFile == null) { 115 throw new IOException("JNI library has no parent file"); 116 } 117 parentFile.mkdirs(); 118 119 try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) { 120 byte[] buffer = new byte[0xFFFF]; // 64K copy buffer 121 int readBytes; 122 while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD 123 os.write(buffer, 0, readBytes); 124 } 125 } 126 System.load(jniLibrary.getAbsolutePath()); 127 } 128 } 129 } 130 } 131 } 132 } 133 134 /** 135 * Load a native library by directly hashing the file. 136 * 137 * @throws IOException if the library failed to load 138 */ 139 public void loadLibraryHashed() throws IOException { 140 try { 141 // First, try loading path 142 System.loadLibrary(m_libraryName); 143 } catch (UnsatisfiedLinkError ule) { 144 // Then load the hash from the input file 145 String resName = RuntimeDetector.getLibraryResource(m_libraryName); 146 String hash; 147 try (InputStream is = m_loadClass.getResourceAsStream(resName)) { 148 if (is == null) { 149 throw new IOException(getLoadErrorMessage(ule)); 150 } 151 MessageDigest md; 152 try { 153 md = MessageDigest.getInstance("MD5"); 154 } catch (NoSuchAlgorithmException nsae) { 155 throw new RuntimeException("Weird Hash Algorithm?"); 156 } 157 try (DigestInputStream dis = new DigestInputStream(is, md)) { 158 // Read the entire buffer once to hash 159 byte[] buffer = new byte[0xFFFF]; 160 while (dis.read(buffer) > -1) {} 161 MessageDigest digest = dis.getMessageDigest(); 162 byte[] digestOutput = digest.digest(); 163 StringBuilder builder = new StringBuilder(); 164 for (byte b : digestOutput) { 165 builder.append(String.format("%02X", b)); 166 } 167 hash = builder.toString().toLowerCase(Locale.ENGLISH); 168 } 169 } 170 if (hash == null) { 171 throw new IOException("Weird Hash?"); 172 } 173 File jniLibrary = new File(m_extractionRoot, resName + "." + hash); 174 try { 175 // Try to load from an already extracted hash 176 System.load(jniLibrary.getAbsolutePath()); 177 } catch (UnsatisfiedLinkError ule2) { 178 // If extraction failed, extract 179 try (InputStream resIs = m_loadClass.getResourceAsStream(resName)) { 180 if (resIs == null) { 181 throw new IOException(getLoadErrorMessage(ule)); 182 } 183 184 var parentFile = jniLibrary.getParentFile(); 185 if (parentFile == null) { 186 throw new IOException("JNI library has no parent file"); 187 } 188 parentFile.mkdirs(); 189 190 try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) { 191 byte[] buffer = new byte[0xFFFF]; // 64K copy buffer 192 int readBytes; 193 while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD 194 os.write(buffer, 0, readBytes); 195 } 196 } 197 System.load(jniLibrary.getAbsolutePath()); 198 } 199 } 200 } 201 } 202}