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 com.fasterxml.jackson.core.type.TypeReference; 008import com.fasterxml.jackson.databind.ObjectMapper; 009import java.io.File; 010import java.io.IOException; 011import java.nio.file.Files; 012import java.nio.file.Paths; 013import java.util.ArrayList; 014import java.util.HashMap; 015import java.util.List; 016import java.util.Map; 017import java.util.Objects; 018 019/** Loads dynamic libraries for all platforms. */ 020public final class CombinedRuntimeLoader { 021 private CombinedRuntimeLoader() {} 022 023 private static String extractionDirectory; 024 025 /** 026 * Returns library extraction directory. 027 * 028 * @return Library extraction directory. 029 */ 030 public static synchronized String getExtractionDirectory() { 031 return extractionDirectory; 032 } 033 034 private static synchronized void setExtractionDirectory(String directory) { 035 extractionDirectory = directory; 036 } 037 038 /** 039 * Sets DLL directory. 040 * 041 * @param directory Directory. 042 * @return DLL directory. 043 */ 044 public static native String setDllDirectory(String directory); 045 046 private static String getLoadErrorMessage(String libraryName, UnsatisfiedLinkError ule) { 047 StringBuilder msg = new StringBuilder(512); 048 msg.append(libraryName) 049 .append(" could not be loaded from path\n" + "\tattempted to load for platform ") 050 .append(RuntimeDetector.getPlatformPath()) 051 .append("\nLast Load Error: \n") 052 .append(ule.getMessage()) 053 .append('\n'); 054 if (RuntimeDetector.isWindows()) { 055 msg.append( 056 "A common cause of this error is missing the C++ runtime.\n" 057 + "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n"); 058 } 059 return msg.toString(); 060 } 061 062 /** 063 * Extract a list of native libraries. 064 * 065 * @param <T> The class where the resources would be located 066 * @param clazz The actual class object 067 * @param resourceName The resource name on the classpath to use for file lookup 068 * @return List of all libraries that were extracted 069 * @throws IOException Thrown if resource not found or file could not be extracted 070 */ 071 @SuppressWarnings("unchecked") 072 public static <T> List<String> extractLibraries(Class<T> clazz, String resourceName) 073 throws IOException { 074 TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {}; 075 ObjectMapper mapper = new ObjectMapper(); 076 Map<String, Object> map; 077 try (var stream = clazz.getResourceAsStream(resourceName)) { 078 map = mapper.readValue(stream, typeRef); 079 } 080 081 var platformPath = Paths.get(RuntimeDetector.getPlatformPath()); 082 var platform = platformPath.getName(0).toString(); 083 var arch = platformPath.getName(1).toString(); 084 085 var platformMap = (Map<String, List<String>>) map.get(platform); 086 087 var fileList = platformMap.get(arch); 088 089 var extractionPathString = getExtractionDirectory(); 090 091 if (extractionPathString == null) { 092 String hash = (String) map.get("hash"); 093 094 var defaultExtractionRoot = RuntimeLoader.getDefaultExtractionRoot(); 095 var extractionPath = Paths.get(defaultExtractionRoot, platform, arch, hash); 096 extractionPathString = extractionPath.toString(); 097 098 setExtractionDirectory(extractionPathString); 099 } 100 101 List<String> extractedFiles = new ArrayList<>(); 102 103 byte[] buffer = new byte[0x10000]; // 64K copy buffer 104 105 for (var file : fileList) { 106 try (var stream = clazz.getResourceAsStream(file)) { 107 Objects.requireNonNull(stream); 108 109 var outputFile = Paths.get(extractionPathString, new File(file).getName()); 110 extractedFiles.add(outputFile.toString()); 111 if (outputFile.toFile().exists()) { 112 continue; 113 } 114 var parent = outputFile.getParent(); 115 if (parent == null) { 116 throw new IOException("Output file has no parent"); 117 } 118 parent.toFile().mkdirs(); 119 120 try (var os = Files.newOutputStream(outputFile)) { 121 int readBytes; 122 while ((readBytes = stream.read(buffer)) != -1) { // NOPMD 123 os.write(buffer, 0, readBytes); 124 } 125 } 126 } 127 } 128 129 return extractedFiles; 130 } 131 132 /** 133 * Load a single library from a list of extracted files. 134 * 135 * @param libraryName The library name to load 136 * @param extractedFiles The extracted files to search 137 * @throws IOException If library was not found 138 */ 139 public static void loadLibrary(String libraryName, List<String> extractedFiles) 140 throws IOException { 141 String currentPath = null; 142 String oldDllDirectory = null; 143 try { 144 if (RuntimeDetector.isWindows()) { 145 var extractionPathString = getExtractionDirectory(); 146 oldDllDirectory = setDllDirectory(extractionPathString); 147 } 148 for (var extractedFile : extractedFiles) { 149 if (extractedFile.contains(libraryName)) { 150 // Load it 151 currentPath = extractedFile; 152 System.load(extractedFile); 153 return; 154 } 155 } 156 throw new IOException("Could not find library " + libraryName); 157 } catch (UnsatisfiedLinkError ule) { 158 throw new IOException(getLoadErrorMessage(currentPath, ule)); 159 } finally { 160 if (oldDllDirectory != null) { 161 setDllDirectory(oldDllDirectory); 162 } 163 } 164 } 165 166 /** 167 * Load a list of native libraries out of a single directory. 168 * 169 * @param <T> The class where the resources would be located 170 * @param clazz The actual class object 171 * @param librariesToLoad List of libraries to load 172 * @throws IOException Throws an IOException if not found 173 */ 174 public static <T> void loadLibraries(Class<T> clazz, String... librariesToLoad) 175 throws IOException { 176 // Extract everything 177 178 var extractedFiles = extractLibraries(clazz, "/ResourceInformation.json"); 179 180 String currentPath = ""; 181 182 try { 183 if (RuntimeDetector.isWindows()) { 184 var extractionPathString = getExtractionDirectory(); 185 // Load windows, set dll directory 186 currentPath = Paths.get(extractionPathString, "WindowsLoaderHelper.dll").toString(); 187 System.load(currentPath); 188 } 189 } catch (UnsatisfiedLinkError ule) { 190 throw new IOException(getLoadErrorMessage(currentPath, ule)); 191 } 192 193 for (var library : librariesToLoad) { 194 loadLibrary(library, extractedFiles); 195 } 196 } 197}