001/******************************************************************************* 002 * Copyright (c) 2024, 2026, Olivier Ayache. All rights reserved. 003 * 004 * This file is part of AVPKit. 005 * 006 * AVPKit is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU Lesser General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * AVPKit is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with AVPKit. If not, see <http://www.gnu.org/licenses/>. 018 *******************************************************************************/ 019 020package com.avpkit.ferry; 021 022import java.io.File; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.Map; 028import java.util.Set; 029 030import org.slf4j.Logger; 031import org.slf4j.LoggerFactory; 032 033/** 034 * Internal Only. Finds and loads native libraries that we depend on. 035 * <p> 036 * It supplements the Java builtin {@link System#loadLibrary(String)} by looking 037 * in more places. 038 * </p> 039 * <p> 040 * Many methods in this class are marked as "package level". In reality, they 041 * are private, but are at this protection level so our test suite can test 042 * internals. 043 * </p> 044 * 045 */ 046public final class JNILibraryLoader 047{ 048 private static final Logger log = LoggerFactory 049 .getLogger(JNILibraryLoader.class); 050 051 /** 052 * An enumeration of the types of OS's we will do special handling for. 053 */ 054 enum OSName 055 { 056 Unknown, Windows, MacOSX, Linux 057 }; 058 059 /** 060 * @deprecated Use {@link JNILibrary} instead. 061 * 062 * Attempts to find and load the given library, with the given version of the 063 * library if version is asked for. 064 * <p> 065 * First, if we detect that we've already loaded the given library, we'll just 066 * return rather than attempt a second load (which will fail on some OSes). 067 * </p> 068 * <p> 069 * If we haven't already loaded this library, we will search in the path 070 * defined by the property java.library.path for the library, creating 071 * OS-dependent names, and using version strings. If we can't find it in that 072 * property, we'll search through the OS-dependent shared-library 073 * specification environment variable. 074 * </p> 075 * <p> 076 * If we can't find a versioned library (and one was requested) but can find 077 * an unversioned library, we'll use the unversioned library. But we will 078 * first search all directories for a versioned library under the assumption 079 * that if you asked for a version, you care more about meeting that 080 * requirement than finding it in the first directory we run across. 081 * </p> 082 * <p> 083 * If all that still fails, we'll fall back to the 084 * {@link System#loadLibrary(String)} method (for example, if we cannot guess 085 * a libtool-like convention for the OS we're on). 086 * </p> 087 * <p> 088 * We assume a libtool-like library name for the shared library, but will 089 * check for common variants on that name. 090 * </p> 091 * <p> 092 * Hopefully an illustration will make this all clearer. Assume we're looking 093 * for a library named "foo" with version 1, this method will search as 094 * follows: 095 * </p> 096 * <p> 097 * <table> 098 * <tr> 099 * <th>OS</th> 100 * <th>Filenames searched for (in order)</th> 101 * <th>Directories looked in (in order)</th> 102 * </tr> 103 * <tr> 104 * <td> 105 * On Windows:</td> 106 * <td> 107 * <ol> 108 * <li>foo-1.dll</li> 109 * <li>libfoo-1.dll</li> 110 * <li>cygfoo-1.dll</li> 111 * <li>foo.dll</li> 112 * <li>libfoo.dll</li> 113 * <li>cygfoo.dll</li> 114 * </ol> 115 * </td> 116 * <td> 117 * <ol> 118 * <li>Every directory in the java property <b>java.library.path</b></li> 119 * <li>Every directory in the environment variable <b>PATH</b></li> 120 * </ol> 121 * </td> 122 * </tr> 123 * <tr> 124 * <td> 125 * On Linux:</td> 126 * <td> 127 * <ol> 128 * <li>libfoo.so.1</li> 129 * <li>libfoo.so</li> 130 * </ol> 131 * </td> 132 * <td> 133 * <ol> 134 * <li>Every directory in the java property <b>java.library.path</b></li> 135 * <li>Every directory in the environment variable <b>LD_LIBRARY_PATH</b></li> 136 * </ol> 137 * </td> 138 * </tr> 139 * <tr> 140 * <td> 141 * On Apple OS-X:</td> 142 * <td> 143 * <ol> 144 * <li>libfoo.1.dylib</li> 145 * <li>libfoo.dylib</li> 146 * </ol> 147 * </td> 148 * <td> 149 * <ol> 150 * <li>Every directory in the java property <b>java.library.path</b></li> 151 * <li>Every directory in the environment variable <b>DYLD_LIBRARY_PATH</b></li> 152 * </ol> 153 * </td> 154 * </tr> 155 * </table> 156 * 157 * @param aLibraryName 158 * The name of the library, without any OS-dependent adornments like 159 * "lib", ".so" or ".dll". 160 * @param aMajorVersion 161 * The major version of this library, or null if you want to take any 162 * version. 163 * 164 * @throws UnsatisfiedLinkError 165 * If we cannot find the library after searching in all the 166 * aforementioned locations. 167 */ 168 @Deprecated 169 public static void loadLibrary(String aLibraryName, Long aMajorVersion) 170 { 171 getInstance().loadLibrary0(aLibraryName, aMajorVersion); 172 } 173 174 /** 175 * Redirects to {@link #loadLibrary(String, Long)}, but leaves the version as 176 * null (not requested). 177 * 178 * @param aLibraryName 179 * The name of the library, without any OS-dependent adornments like 180 * "lib", ".so" or ".dll". 181 */ 182 public static void loadLibrary(String aLibraryName) 183 { 184 loadLibrary(aLibraryName, null); 185 } 186 187 /** 188 * The singleton instance of the {@link JNILibraryLoader}. We only allow one 189 * per class-loader instance. Technically it would be nice to ensure only one 190 * per process, but that's currently beyond my Java ken. 191 */ 192 private static JNILibraryLoader mGlobalLoader = new JNILibraryLoader(); 193 194 /** 195 * The set of directories in the java.library.path property; this is queried 196 * once per object and then cached, as the property is read-only per Sun's 197 * documentation and will not change during the lifetime of this process. 198 */ 199 private String[] mJavaPropPaths; 200 201 /** 202 * The set of directories in the environment variable this OS uses to denote 203 * places where shared objects may live (e.g. LD_LIBRARY_PATH on Linux); this 204 * is queried once per object and then cached, as the property is read-only 205 * per Sun's documentation and will not change during the lifetime of this 206 * process. 207 */ 208 private String[] mJavaEnvPaths; 209 210 /** 211 * What we think the Operating System we're running on is. This is used to 212 * guess which environment variable to use for shared objects, and how to turn 213 * a library name into a fully qualified file name. 214 */ 215 private OSName mOS = null; 216 217 /** 218 * A cache that maps a library name to a set of versions we've loaded. 219 */ 220 private Map<String, Set<Long>> mLoadedLibraries = new HashMap<String, Set<Long>>(); 221 222 /** 223 * Get the singleton instance of the {@link JNILibraryLoader}. 224 * 225 * @return The singleton instance to use for all queries. 226 */ 227 static JNILibraryLoader getInstance() 228 { 229 return mGlobalLoader; 230 } 231 232 /** 233 * Constructor for {@link JNILibraryLoader}. Should not be callable from 234 * outside this class so as to ensure only the singleton object can exist. 235 */ 236 private JNILibraryLoader() 237 { 238 log.trace("<init>"); 239 } 240 241 /** 242 * This is the method that actually loads the library. It maintains an object 243 * level lock, and since this class only allows a singleton object, that is a 244 * class-level lock. That means if you're loading a library on one thread, 245 * other threads will block until it finishes. 246 * 247 * This should be OK in general. 248 * 249 * @param aLibraryName 250 * The library name. 251 * @param aMajorVersion 252 * The version, or null if you don't care. 253 */ 254 synchronized void loadLibrary0(String aLibraryName, Long aMajorVersion) 255 { 256 if (alreadyLoadedLibrary(aLibraryName, aMajorVersion)) 257 // our work is done. 258 return; 259 260 List<String> libCandidates = getLibraryCandidates(aLibraryName, 261 aMajorVersion); 262 if (libCandidates != null && libCandidates.size() > 0 263 && !loadCandidateLibrary(aLibraryName, aMajorVersion, libCandidates)) 264 { 265 // finally, try the System.loadLibrary call 266 try 267 { 268 System.loadLibrary(aLibraryName); 269 } 270 catch (UnsatisfiedLinkError e) 271 { 272 log 273 .error( 274 "Could not load library: {}; version: {}; Visit http://www.avpkit.com/core/faq/ to find common solutions to this problem", 275 aLibraryName, aMajorVersion == null ? "" : aMajorVersion); 276 throw e; 277 278 } 279 // and if we get here it means we successfully loaded since no 280 // exception was thrown. Add our library to the cache. 281 setLoadedLibrary(aLibraryName, aMajorVersion); 282 } 283 log.trace("Successfully Loaded library: {}; Version: {}", aLibraryName, aMajorVersion); 284 } 285 286 287 /** 288 * Tell the cache that we've loaded this version. 289 * 290 * @param aLibraryName 291 * @param aMajorVersion 292 */ 293 void setLoadedLibrary(String aLibraryName, Long aMajorVersion) 294 { 295 Set<Long> foundVersions = mLoadedLibraries.get(aLibraryName); 296 if (foundVersions == null) 297 { 298 foundVersions = new HashSet<Long>(); 299 mLoadedLibraries.put(aLibraryName, foundVersions); 300 } 301 foundVersions.add(aMajorVersion); 302 } 303 304 /** 305 * Iterates through the set of aLibCandidates until it succeeds in loading a 306 * library. If it succeeds, it lets the cache know. 307 * 308 * @param aLibraryName 309 * The library name. 310 * @param aMajorVersion 311 * The version we want, or null if we don't care. 312 * @param aLibCandidates 313 * The set of candidates generated by 314 * {@link #getLibraryCandidates(String, Long)} 315 * @return true if we succeeded in loading a library; false otherwise 316 */ 317 boolean loadCandidateLibrary(String aLibraryName, Long aMajorVersion, 318 List<String> aLibCandidates) 319 { 320 boolean retval = false; 321 for (String candidate : aLibCandidates) 322 { 323 log 324 .trace( 325 "Attempt: library load of library: {}; version: {}: relative path: {}", 326 new Object[] 327 { 328 aLibraryName, 329 aMajorVersion == null ? "<unspecified>" : aMajorVersion 330 .longValue(), candidate 331 }); 332 File candidateFile = new File(candidate); 333 if (candidateFile.exists()) 334 { 335 String absPath = candidateFile.getAbsolutePath(); 336 try 337 { 338 log 339 .trace( 340 "Attempt: library load of library: {}; version: {}: absolute path: {}", 341 new Object[] 342 { 343 aLibraryName, 344 aMajorVersion == null ? "<unspecified>" : aMajorVersion 345 .longValue(), absPath 346 }); 347 // Here's where we attempt the actual load. 348 System.load(absPath); 349 log 350 .trace( 351 "Success: library load of library: {}; version: {}: absolute path: {}", 352 new Object[] 353 { 354 aLibraryName, 355 aMajorVersion == null ? "<unspecified>" : aMajorVersion 356 .longValue(), absPath 357 }); 358 // if we got here, we loaded successfully 359 setLoadedLibrary(aLibraryName, aMajorVersion); 360 retval = true; 361 break; 362 } 363 catch (UnsatisfiedLinkError e) 364 { 365 log 366 .warn( 367 "Failure: library load of library: {}; version: {}: absolute path: {}; error: {}", 368 new Object[] 369 { 370 aLibraryName, 371 aMajorVersion == null ? "<unspecified>" : aMajorVersion 372 .longValue(), absPath, e 373 }); 374 } 375 catch (SecurityException e) 376 { 377 log 378 .warn( 379 "Failure: library load of library: {}; version: {}: absolute path: {}; error: {}", 380 new Object[] 381 { 382 aLibraryName, 383 aMajorVersion == null ? "<unspecified>" : aMajorVersion 384 .longValue(), absPath, e 385 }); 386 } 387 } 388 } 389 return retval; 390 } 391 392 /** 393 * For a given library, and the OS we're running on, this method generates a 394 * list of potential absolute file paths that 395 * {@link #loadCandidateLibrary(String, Long, String[])} should attempt (in 396 * order) to load. This method will not check for existence and readability of 397 * the file we're attempting to load. 398 * 399 * @param aLibraryName 400 * The library name 401 * @param aMajorVersion 402 * The version, or null if we don't care. 403 * @return The set of absolute file paths to try. 404 */ 405 List<String> getLibraryCandidates(String aLibraryName, Long aMajorVersion) 406 { 407 final List<String> retval = new LinkedList<String>(); 408 // Note: when done each of these variables must be set to a non-null, non 409 // empty string array 410 final String[] prefixes; 411 final String[] suffixes; 412 final String[] preSuffixVersions; 413 final String[] postSuffixVersions; 414 415 switch (getOS()) 416 { 417 case Unknown: 418 case Linux: 419 prefixes = new String[] 420 { 421 "lib", "" 422 }; 423 suffixes = new String[] 424 { 425 ".so" 426 }; 427 preSuffixVersions = new String[] 428 { 429 "" 430 }; 431 postSuffixVersions = (aMajorVersion == null ? new String[] 432 { 433 "" 434 } : new String[] 435 { 436 "." + aMajorVersion.longValue() 437 }); 438 break; 439 case Windows: 440 prefixes = new String[] 441 { 442 "lib", "", "cyg" 443 }; 444 suffixes = new String[] 445 { 446 ".dll" 447 }; 448 preSuffixVersions = (aMajorVersion == null ? new String[] 449 { 450 "" 451 } : new String[] 452 { 453 "-" + aMajorVersion.longValue() 454 }); 455 postSuffixVersions = new String[] 456 { 457 "" 458 }; 459 break; 460 case MacOSX: 461 prefixes = new String[] 462 { 463 "lib", "" 464 }; 465 suffixes = new String[] 466 { 467 ".dylib" 468 }; 469 preSuffixVersions = (aMajorVersion == null ? new String[] 470 { 471 "" 472 } : new String[] 473 { 474 "." + aMajorVersion.longValue() 475 }); 476 postSuffixVersions = new String[] 477 { 478 "" 479 }; 480 break; 481 default: 482 // really no cases should get here 483 prefixes = null; 484 suffixes = null; 485 preSuffixVersions = null; 486 postSuffixVersions = null; 487 break; 488 } 489 initializeSearchPaths(); 490 491 // First check the versioned paths 492 if (aMajorVersion != null) 493 { 494 for (String directory : mJavaPropPaths) 495 { 496 generateFileNames(retval, directory, aLibraryName, prefixes, suffixes, 497 preSuffixVersions, postSuffixVersions, true); 498 } 499 for (String directory : mJavaEnvPaths) 500 { 501 generateFileNames(retval, directory, aLibraryName, prefixes, suffixes, 502 preSuffixVersions, postSuffixVersions, true); 503 } 504 } 505 for (String directory : mJavaPropPaths) 506 { 507 generateFileNames(retval, directory, aLibraryName, prefixes, suffixes, 508 preSuffixVersions, postSuffixVersions, false); 509 } 510 for (String directory : mJavaEnvPaths) 511 { 512 generateFileNames(retval, directory, aLibraryName, prefixes, suffixes, 513 preSuffixVersions, postSuffixVersions, false); 514 } 515 return retval; 516 } 517 518 void generateFileNames(List<String> aResults, String aDirectory, 519 String aLibraryName, String[] aPrefixes, String[] aSuffixes, 520 String[] aPreSuffixVersions, String[] aPostSuffixVersions, 521 boolean aIncludeVersion) 522 { 523 // make sure aDirectory ends with correct terminator 524 String dirSeparator = File.separator; 525 if (!aDirectory.endsWith(dirSeparator)) 526 aDirectory = aDirectory + dirSeparator; 527 for (String suffix : aSuffixes) 528 { 529 for (String prefix : aPrefixes) 530 { 531 if (aIncludeVersion) 532 { 533 for (String preSuffixVersion : aPreSuffixVersions) 534 { 535 for (String postSuffixVersion : aPostSuffixVersions) 536 { 537 String result = aDirectory + prefix + aLibraryName 538 + preSuffixVersion + suffix + postSuffixVersion; 539 aResults.add(result); 540 } 541 } 542 } 543 else 544 { 545 String result = aDirectory + prefix + aLibraryName + suffix; 546 aResults.add(result); 547 } 548 } 549 } 550 } 551 552 /** 553 * Initialize the paths we'll search for libraries in. 554 */ 555 private void initializeSearchPaths() 556 { 557 String pathVar = null; 558 if (mJavaPropPaths == null) 559 { 560 pathVar = System.getProperty("java.library.path", ""); 561 log.trace("property java.library.path: {}", pathVar); 562 mJavaPropPaths = getEntitiesFromPath(pathVar); 563 } 564 if (mJavaEnvPaths == null) 565 { 566 String envVar = getSystemRuntimeLibraryPathVar(); 567 pathVar = System.getenv(envVar); 568 log.trace("OS environment runtime shared library path ({}): {}", envVar, 569 pathVar); 570 mJavaEnvPaths = getEntitiesFromPath(pathVar); 571 } 572 573 } 574 575 OSName getOS() 576 { 577 if (mOS != null) 578 return mOS; 579 OSName retval = OSName.Linux; 580 String osName = System.getProperty("os.name", "Linux"); 581 if (osName.length() > 0) 582 { 583 if (osName.startsWith("Windows")) 584 retval = OSName.Windows; 585 else if (osName.startsWith("Mac")) 586 retval = OSName.MacOSX; 587 else if (osName.startsWith("Linux")) 588 retval = OSName.Linux; 589 else 590 // default everything to Linux 591 retval = OSName.Linux; 592 } 593 mOS = retval; 594 log.trace("Detected OS: {}", mOS); 595 return retval; 596 } 597 598 599 /** 600 * For internal use only. This method allows tests to override the guessed OS 601 * name. 602 * 603 * @param os 604 * The OS to set 605 */ 606 void setOS(OSName os) 607 { 608 mOS = os; 609 } 610 611 String getSystemRuntimeLibraryPathVar() 612 { 613 String retval = "LD_LIBRARY_PATH"; 614 switch (getOS()) 615 { 616 case Windows: 617 retval = "PATH"; 618 break; 619 case MacOSX: 620 retval = "DYLD_LIBRARY_PATH"; 621 break; 622 case Linux: 623 case Unknown: 624 break; 625 } 626 return retval; 627 } 628 629 String[] getEntitiesFromPath(String aPathVar) 630 { 631 String[] retval = null; 632 String sep = File.pathSeparator; 633 if (aPathVar == null || aPathVar.length() == 0) 634 { 635 retval = new String[1]; 636 retval[0] = "."; 637 log 638 .trace("Have empty path var; assuming current directory to find native libraries"); 639 } 640 else 641 { 642 log.trace("Parsing path var: {}", aPathVar); 643 int len = aPathVar.length(); 644 645 // find out how many paths there are 646 int i = 0; 647 int n = 0; 648 int j = 0; 649 650 n = 1; 651 i = aPathVar.indexOf(sep); 652 while (i >= 0) 653 { 654 n++; 655 i = aPathVar.indexOf(sep, i + 1); 656 } 657 658 log.trace("Found {} paths in path var: {}", n, aPathVar); 659 // Create the return array 660 retval = new String[n]; 661 662 // now fill in the actual strings 663 i = 0; 664 n = 0; 665 j = aPathVar.indexOf(sep); 666 while (j >= 0) 667 { 668 if (j - i > 0) 669 { 670 // a non zero entry was found 671 retval[n] = aPathVar.substring(i, j); 672 log.trace("Added path {} for path var: {}", retval[n], aPathVar); 673 ++n; 674 } 675 else if (j - i == 0) 676 { 677 // someone put two path separators, with nothing in 678 // between. just assume they meant the current directory 679 retval[n] = "."; 680 log.trace("Added path {} for path var: {}", retval[n], aPathVar); 681 ++n; 682 } 683 i = j + 1; 684 j = aPathVar.indexOf(sep, i); 685 } 686 // and get the last entry which should have no separator 687 retval[n] = aPathVar.substring(i, len); 688 log.trace("Adding last path {} for path var: {}", retval[n], aPathVar); 689 if (retval[n] == null || retval[n].length() == 0) 690 { 691 // A malformed path with a separator at the end; we just add 692 // the current directory again. 693 retval[n] = "."; 694 log.trace("Faking last path {} for malformed path var: {}", retval[n], 695 aPathVar); 696 } 697 698 } 699 return retval; 700 } 701 702 /** 703 * Checks our cache to see if we've already loaded this library. 704 * 705 * We will also detect if we've already loaded another version of this 706 * library, and log a warning, but otherwise will return false in that case. 707 * 708 * @param aLibraryName 709 * The library name. 710 * @param aMajorVersion 711 * The version, or null if we don't care. 712 * @return true if in cache; false otherwise 713 */ 714 boolean alreadyLoadedLibrary(String aLibraryName, Long aMajorVersion) 715 { 716 boolean retval = false; 717 Set<Long> foundVersions = mLoadedLibraries.get(aLibraryName); 718 if (foundVersions != null) 719 { 720 // we found at least some versions 721 if (aMajorVersion == null || foundVersions.contains(aMajorVersion)) 722 { 723 retval = true; 724 } 725 else 726 { 727 log 728 .warn( 729 "Attempting load of {}, version {}, but already loaded verions: {}." 730 + " We will attempt to load the specified version but behavior is undefined", 731 new Object[] 732 { 733 aLibraryName, aMajorVersion, foundVersions.toArray() 734 }); 735 } 736 } 737 return retval; 738 } 739}