摘要:本文通过编译后运行找不到库文件的问题引入,首先分析了findpackage(JNI)的工作流程,而后针对cmake不搜索LDLIBRARYPATH的问题,提出了一种通用的解决办法。 本文分享自华为云社区《CMake库搜索函数居然不搜索LDLIBRARYPATH?由编译工具使用体验而引发的思考云社区华为云》,作者:蜉蝣与海。 最近产品要使用JNI技术,CMake编译C代码时需要对外链接libjvm。so库。代码编译倒是正常,系统中也有libjvm。so,然而使用时却报了如下异常:errorwhileloadingsharedlibraries:libjvm。so:cannotopensharedobjectfile:Nosuchfileordirectory 这个报错表示,操作系统并没有找到libjvm。so,我们的操作系统是从LDLIBRARYPATH中搜索这些动态链接库,很显然目前libjvm。so并不在这个目录下。 问题的解决倒是简单,直接在LDLIBRARYPATH里加入libjvm。so的库即可。但是这却引发了我的思考:为什么构建时可以找到libjvm。so,运行时却找不到呢? 这个问题的回答,既可以有简明扼要版解释,又可以刨根问底深挖。 先来看简明扼要版解释: 代码的CMakeList中使用了下列语句,在编译过程中寻找并链接libjvm。so,这个搜索方式和操作系统的搜索方式不同:findpackage(JNI)getfilenamecomponent(JVMLIBPATH{JAVAJVMLIBRARY}DIRECTORY)getfilenamecomponent(JAVALIBPATH{JVMLIBPATH}DIRECTORY)linkdirectories({JVMLIBPATH}{JAVALIBPATH})settargetproperties({NAME}PROPERTIESLINKFLAGSljvm) 其中findpackage(JNI)会搜索libjvm。so可能存在的路径,通过getfilenamecomponent来获得libjvm。so的文件夹,并把这个文件夹设为默认搜索库路径。而后settargetproperties会进行链接工作。 这个答案只能告诉我们是什么,但是作为一只程序猿,还要了解为什么,这里引申几个问题讨论:1、findpackage(JNI)的工作过程是怎样的?为什么LDLIBRARYPATH里没找到的依赖库,cmake可以找到2、cmake的库搜索函数findlibrary会搜索LDLIBRARYPATH吗,如果不会,可以通过设置来搜索LDLIBRARYPATH吗?问题一:findpackage(JNI)的工作过程是怎样的 为了方便开发者引用外部包,cmake官方预定义了许多寻找依赖包的Module,他们存储在cmake的sharecmakeModules目录下。每个以Find。cmake命名的文件都可以帮我们找到一个包〔1〕。在本地计算机执行以下指令,即可找到findpackage(JNI)使用的脚本文件。findnameFindJNI。cmake 打开自己的cmake对应的FindJNI文件,可以看到密密麻麻的注释和脚本,通过阅读这些脚本,我们得以得知FindJNI是如何工作的。 分析问题前,先看问题带来的结果,文件最上方注释有如下说明:Thismodulesetsthefollowingresultvariables:JNIINCLUDEDIRStheincludedirstouseJNILIBRARIESthelibrariestouse(JAWTandJVM)JNIFOUNDTRUEifJNIheadersandlibrarieswerefound。CacheVariablesThefollowingcachevariablesarealsoavailabletosetoruse:JAVAAWTLIBRARYthepathtotheJavaAWTNativeInterface(JAWT)libraryJAVAJVMLIBRARYthepathtotheJavaVirtualMachine(JVM)libraryJAVAINCLUDEPATHtheincludepathtojni。hJAVAINCLUDEPATH2theincludepathtojnimd。handjniport。hJAVAAWTINCLUDEPATHtheincludepathtojawt。h 这段代码表明,执行findpackage(JNI)之后,会有一系列变量被设置,其中包括表示JNI是否被找到的变量JNIFOUND,以及表示libjvm。so的变量JAVAJVMLIBRARY。这些变量在设定之后,通过FindPackageHandleStandardArgs导出,返回调用处,FindPackageHandleStandardArgs是cmake专门用来导出变量的宏〔2〕:include({CMAKECURRENTLISTDIR}FindPackageHandleStandardArgs。cmake)FINDPACKAGEHANDLESTANDARDARGS(JNIDEFAULTMSGJAVAAWTLIBRARYJAVAJVMLIBRARYJAVAINCLUDEPATHJAVAINCLUDEPATH2JAVAAWTINCLUDEPATH) 在文件中定位JAVAJVMLIBRARY,可以追踪到下述代码片段:foreach(search{JNISEARCHES})findlibrary(JAVAJVMLIBRARY{JNI{search}JVM})findlibrary(JAVAAWTLIBRARY{JNI{search}JAWT})if(JAVAJVMLIBRARY)break()endif()endforeach() 由此可知,JAVAJVMLIBRARY这个变量,是通过逐个搜索{JNI{search}JVM}里的文件夹进而确定JAVAJVMLIBRARY的。而{JNI{search}JVM}相关的定义语句如图:set(JNIFRAMEWORKJVMNAMESJavaVM)set(JNINORMALJVMNAMESjvmPATHS{JAVAJVMLIBRARYDIRECTORIES}) 其中JAVAJVMLIBRARYDIRECTORIES中涉及了大量可能的libjvm。so存在的路径。set(JAVAJVMLIBRARYDIRECTORIES)foreach(dir{JAVAAWTLIBRARYDIRECTORIES})list(APPENDJAVAJVMLIBRARYDIRECTORIES{dir}{dir}client{dir}serverIBMSDK,JavaTechnologyEdition,specificpaths{dir}j9vm{dir}default)endforeach()set(JAVAAWTLIBRARYDIRECTORIES)if(JAVAHOME)JAVAAPPENDLIBRARYDIRECTORIES(JAVAAWTLIBRARYDIRECTORIES{JAVAHOME}jrelib{libarch}{JAVAHOME}jrelib{JAVAHOME}lib{libarch}{JAVAHOME}lib{JAVAHOME})endif()JAVAAPPENDLIBRARYDIRECTORIES(JAVAAWTLIBRARYDIRECTORIES{JNIJAVAAWTLIBRARYTRIES})foreach(javadirINLISTSJNIJAVADIRECTORIESBASE)list(APPENDJNIJAVAAWTLIBRARYTRIES{javadir}jrelib{libarch}{javadir}jrelib{javadir}lib{libarch}{javadir}lib{javadir})list(APPENDJNIJAVAINCLUDETRIES{javadir}include)endforeach() 如上图所示,变量依赖顺序如下: JAVAJVMLIBRARYDIRECTORIESJAVAAWTLIBRARYDIRECTORIESJNIJAVAAWTLIBRARYTRIESJAVAHOMEJNIJAVADIRECTORIESBASE 最终发现JAVAJVMLIBRARYDIRECTORIES变量的值,是由JAVAHOME变量的值和JNIJAVADIRECTORIESBASE变量的值共同决定的。而JNIJAVADIRECTORYBASE预置了大量预定义路径:set(JNIJAVADIRECTORIESBASEusrlibjvmjavausrlibjavausrlibjvmusrlocallibjavausrlocalsharejavausrlibj2sdk1。4sunusrlibj2sdk1。5sunoptsunjdk1。5。0。04usrlibjvmjava6sunusrlibjvmjava1。5。0sunusrlibjvmjava6sun1。6。0。00canthisoneberemovedaccordingto8821?Alexusrlibjvmjava6openjdkusrlibjvmjava1。6。0openjdk1。6。0。0fedoraDebianspecificpathsfordefaultJVMusrlibjvmdefaultjavaArchLinuxspecificpathsfordefaultJVMusrlibjvmdefaultUbuntuspecificpathsfordefaultJVMusrlibjvmjava11openjdk{libarch}Ubuntu18。04LTSusrlibjvmjava8openjdk{libarch}Ubuntu15。10usrlibjvmjava7openjdk{libarch}Ubuntu15。10usrlibjvmjava6openjdk{libarch}Ubuntu15。10OpenBSDspecificpathsfordefaultJVMusrlocaljdk1。7。0usrlocaljre1。7。0usrlocaljdk1。6。0usrlocaljre1。6。0SuSEspecificpathsfordefaultJVMusrlib64jvmjavausrlib64jvmjre) 通过以上分析可以看出,JAVAJVMLIBRARY的搜索,依赖JAVAHOME和大量预定义路径。问题二:cmake库搜索函数findlibrary会搜索LDLIBRARYPATH吗 通过阅读DoesCMakesfindlibrarysearchLDLIBRARYPATH可以知道,findlibrary默认不搜索LDLIBRARYPATH,并且网上也找不到让cmake搜索LDLIBRARYPATH的文章。那cmake能搜索LDLIBRARYPATH吗? 答案是可以的,通过cmake获取LDLIBRARYPATH环境变量,并转为cmake可理解的list格式,而后注入findlibrary即可,代码如下:string(REPLACE:;RUNTIMEPATHENV{LDLIBRARYPATH})findlibrary(JVMAPINAMESjvmHINTS{RUNTIMEPATH})if(JVMAPISTREQUALJVMAPINOTFOUND)message(WARNINGfoundlibjvm。soonlyin{JAVAJVMLIBRARY}butnotinLDLIBRARYPATH。environmentvariableLDLIBRARYPATHmustincludeitsdirectory。)endif() 如果希望找不到这个库时编译失败,可以将WARNING改为fatalerror,代码如下:string(REPLACE:;RUNTIMEPATHENV{LDLIBRARYPATH})findlibrary(JVMAPINAMESjvmHINTS{RUNTIMEPATH})if(JVMAPISTREQUALJVMAPINOTFOUND)message(FATALERRORfoundlibjvm。soonlyin{JAVAJVMLIBRARY}butnotinLDLIBRARYPATH。environmentvariableLDLIBRARYPATHmustincludeitsdirectory。)endif()小结 本文通过编译后运行找不到库文件的问题引入,首先分析了findpackage(JNI)的工作流程,而后针对cmake不搜索LDLIBRARYPATH的问题,提出了一种通用的解决办法。参考文献: 〔1〕Cmake之深入理解findpackage()的用法:https:zhuanlan。zhihu。comp97369704?utmsourcewechatsession 〔2〕Cmake中findpackage命令的搜索模式之模块模式(Modulemode):https:www。jianshu。compf983a90bcf91 〔3〕DoesCMakesfindlibrarysearchLDLIBRARYPATH?:https:stackoverflow。comquestions41566316doescmakesfindlibrarysearchldlibrarypath 点击下方,第一时间了解华为云新鲜技术 华为云博客大数据博客AI博客云计算博客开发者中心华为云