• 您的位置:首页 > 新闻动态 > UE4

    深入浅出了解UE4静态库与动态库的导入方法和调用方法

    2018/3/20      点击:

    一.基本内容概述

            最近做项目时经常看到build.cs文件,就想研究一下UE4中第三方库(SO,LIB,DLL)的使用。通过网络以及wiki确实获取到不少有用的信息,但是没有一篇文章,让我看完就立刻明白的。所以,我在这里详细的描述dll与lib在UE4中的使用,同时简单描述一些基本原理,解决网上的一些文章由于描述不清而造成的误导。

            UE4本身有很多功能使用的是第三方的库,如物理physX,模型优化Simplygon,SpeedTree以及steam平台相关内容等等。我们如果想使用一些自己的已经实现的库(或者其他现成的库)我们通常可以把库文件放到下面这个的地方——\Engine\Source\ThirdParty目录(下面的例子就是放到这个目录的)。

    (这里需要注意一下,库文件的位置其实是随意的,你只要让引擎能找到他就行。比如你随意的放了一个位置,那你可以通过下面几个方法获取一些特定的路径,然后进一步寻找你的库文件。UEBuildConfiguration.UEThirdPartySourceDirectory方法可以获取到当前所在第三方库的目录,ModuleDirectory表示当前模块的目录,*FPaths::GamePluginsDir()可以获取到当前项目的插件目录。总之,多查查,多试试,各种API,没有你找不到的文件

            如果想要使用自己的库,就不得不提到UE4的编译系统,UnrealBuildTool。UE4里面的项目都是以Module模块为单位的,不同模块之间关联在一起,构成整个系统。UnrealBuildTool负责将不同的模块编译到一起,每一个Module模块需要一个.build.cs文件。比如联机需要的OnlineSubSystemSteam就是一个插件模块,你同时还可以看到OnlineSubsystemFacebook等模块。(当然,UnrealBuildTool做的工作可能比你想的还多,比如跨平台相关的处理等。)

            那么我想添加第三方库就出现了两种选择:

            第一,自己在工程项目下新建一个目录,直接在项目工程的.build.cs下配置这个第三方库,比较直接了当。缺点是,多个静态库,就需要写多行代码,可能需要经常修改。

            第二,通常我们添加一个库之后,应该给这个库建立一个Module(当然也就需要建立一个对应的库模块的.build.cs文件),方便管理与修改。项目直接调用这个Module即可。可以随时通过修改项目工程的.build.cs安装与卸载整个模块。缺点是还要单独建立一个第三方库的Module,不如方法一简单。

            下面的例子中,我使用的是方法二。一般来说,第三方库模块的.build.cs文件与项目工程的.build.cs文件差异还是很大的。

    (静态库与动态库是有区别的。静态库是在编译期就要链接到工程的dll里面的,所以需要去修改项目的build文件来配置。而第三方的动态库是在运行时而不是在编译的时候使用,所以一般不需要配置build文件,但需要在cpp里面获取。)


    二.第三方库与插件的关系


            对于不熟悉UE4的人,可能对第三方库与插件的关系有点模糊。对于第三方库,一般我们在F:\UnrealEngine4.14\Engine\Source\ThirdParty目录下存放其源代码,头文件,lib等。而第三方库的dll一般存在于F:\UnrealEngine4.14\Engine\Binaries\ThirdParty目录下。正如字面上的理解,第三方库更偏向于于一个代码工具库,直接服务于我们的代码。

            而插件,是直接服务于功能的。插件分为引擎插件与项目插件,分别位于F:\UnrealEngine4.14\Engine\Plugins与F:\UE4Project\项目名称\Plugins,插件的源码就位于这两个文件夹,而其二进制文件通过引擎生成后同样位于该目录的Binary文件夹下。插件可以在不直接修改引擎代码的前提下,为引擎添加完整独立的新功能,或者修改引擎中内建的功能。每一个插件至少包含一个Module,你可以根据你插件里面的内容对模块进行进一步的划分(参考下图Media插件的布局分布,一个AndroidMedia还有好几个Module)。我们的插件模块与项目一样,也可能需要包含第三方库,需要在插件源码的build文件里面配置。

    三.静态库:创建与使用流程

    1.新建一个静态库lib(如果有库文件就跳过这步)

    在VS中,点击新建项目——VisualC++——Win32项目(比如名称为MyThirdParty)。

    点击确定后,在导航窗口中选择静态库。

    添加自己的类代码,修改为x64平台并生成MyThirdParty.lib文件。(Debug与Release都可以)

    (一般的非虚幻项目中,引用外部库只需要设置,项目->属性->配置属性->VC++目录,添加包含目录,库目录,ok,代码中载入库文件 #pragmacomment(lib," MyThirdParty.lib ");就可以了,然而像前面提到的,虚幻有自己的编译系统,这么使用可以运行,但是无法打包

    2.在\Engine\Source\ThirdParty目录下新建自己的库模块

    在\Engine\Source\ThirdParty目录下,新建文件夹并命名,这里以MyTestThirdParty为例。把用到的头文件以及lib分别放到文件夹include,文件夹lib下。

    给第三方库模块创建一个新的MyTestThirdParty.build.cs文件。

    3.编辑MyTestThirdParty.build.cs文件

    MyTestThirdParty.build.cs文件内容如下:

      public classMyTestThirdParty : ModuleRules

      {

            publicMyTestThirdParty(TargetInfoTarget)

            {

                 //表示第三方库

                Type= ModuleType.External;

       //第三方库新模块根目录目录路径你可以通过其他方式来获取路径比如get { return Path.GetFullPath(Path.Combine(ModuleDirectory,"../../ThirdParty/"));}

                stringMyPath= UEBuildConfiguration.UEThirdPartySourceDirectory +"MyTestThirdParty/";

                //包含的头文件路径,因为编译的库里面都是链接过的编译单元,可以认为编译单元是不包含头文件的,所以在之后的使用时还需要获取到头文件的声明信息

                PublicIncludePaths.Add(MyPath +"include/");

                if(Target.Platform== UnrealTargetPlatform.Win64)

                {

                  //第三方静态库的路径

                   PublicLibraryPaths.Add(MyPath+"lib/");

                  //第三方静态库的名称

                   PublicAdditionalLibraries.Add("MyThirdParty.lib");

            }

         }

    }

    到此为止,我们的新的模块的名称就叫做MyTestThirdParty

    4.编辑工程.build.cs文件 

       一个新的项目的build.cs文件大概是这样的,最后一行的代码是需要自己添加的

       usingUnrealBuildTool;

       publicclassMyCClassProjectA :ModuleRules

      {

             public MyCClassProjectA(TargetInfoTarget)

             {

                PublicDependencyModuleNames.AddRange(new string[]

                                         { "Core","CoreUObject","Engine","InputCore","HeadMountedDisplay" }  );

                //将新的第三方库的模块添加进来

                AddThirdPartyPrivateStaticDependencies(Target,"MyTestThirdParty");

             }

    }

    正如前面我所提到的,如果你没有单独给第三方库添加一个模块,你就可以直接在项目的build.cs添加静态库的相关配置(也就是第三步的内容)。

    编译后你会发现,无论是第三方库还是项目,他的Binary文件夹都会有一个UE4Editor.modules文件。

    5.配置工程属性

    右键项目属性——NMake——IntelliSense——包含搜索路径。添加库的目录位置。这样项目就可以搜索到你的库头文件并使用了。


    6.在项目工程类里面#include你的库里面的头文件并测试

    包含完文件,就可以正常的使用库文件里面的内容了。修改完点击生成。如果你发现有不识别的你所包含的头文件的错误,那就重新确认一下Module模块的名称与路径,肯定是这里出了问题。

    (如果需要新建一个类,要注意你的类的.cpp文件的第一个包含#include“项目工程名.h”应该是项目工程名的头文件。否则会编译失败。)

    四.动态库:创建与使用流程

    1.新建一个dll(如果有库文件就跳过这步)

    在VS中,点击新建项目——VisualC++——Win32项目(比如名称为MyThirdParty)。

    点击确定后,在导航窗口中选择Dll库。

    添加自己的类代码

    本例中,采用最简单的类。一个头文件与一个.cpp文件,cpp文件里面都是全局的方法。(参考UE4wiki)

    .h文件内容

    #pragma once 

    #define DLL_EXPORT__declspec(dllexport)   //shortens__declspec(dllexport) to DLL_EXPORT  Dll的导出需要借助__declspec,当然你也可以不使用宏,直接写__declspec(dllexport) floatgetCircleArea(float radius);

    #ifdef __cplusplus      //if C++ is used convert it toC to prevent C++'s name mangling of method names 采用C导出动态库更为安全(有空详细说说)

    extern "C"

    {

        #endif

            float  DLL_EXPORT  getCircleArea(floatradius);

        #ifdef __cplusplus

    }

    #endif

    .cpp文件内容

    #pragma once

    #include "string.h"

    #include"TestDll.h"

    //一个简单的方法,根据半径计算一个圆的面积

    float getCircleArea(floatradius)

    {

            return float(3.1416f* (radius *radius));

    }

    网上一般说要采用Release版本编译,其实这取决于你的版本,一般来说release版本是发行用的,对dll做了很多优化,要比Debug版本的小很多,Debug版本的也可以的(两者都可以进行调试)。同时平台修改为x64平台(除非你是给32位机器使用的)。

    2.建立你自己的工程

         我这里新建一个工程名为MyCClassProjectA,然后新建一个类MyTestDll,让这个类继承与UBlueprintFunctionLibrary,这样就可以直接在Event蓝图里面调用。类内容如下,

    MyTestDll.h

    #include "Kismet/BlueprintFunctionLibrary.h"

    #include "MyTestDll.generated.h"

    UCLASS()

    class UMyTestDll :publicUBlueprintFunctionLibrary

    {

            GENERATED_BODY()

        public:

            //导入dllBlueprintCallable入表示蓝图可以调用,这是UE的基础应该了解

            UFUNCTION(BlueprintCallable,Category="My DLL Library")

                static bool importDLL(FStringfolder,FString name);

            //获取dll中的方法的指针

            UFUNCTION(BlueprintCallable,Category="My DLL Library")

                static bool importMethodGetCircleArea();

            //调用dll里面的方法

            UFUNCTION(BlueprintCallable,Category="My DLL Library")

                static float getCircleAreaFromDll(float radius);

            //释放dll

            UFUNCTION(BlueprintCallable,Category="My DLL Library")

                static void freeDLL();

    };

    MyTestDll.cpp

    #include "MyCClassProjectA.h"

    #include "MyTestDll.h"

    typedef float(*_getCircleArea)(floatradius);//Declare a method to store the DLL method getCircleArea.

    //计算圆面积的函数指针

    _getCircleArea m_getCircleAreaFromDll;

    //dll的句柄

    void *v_dllHandle;

    #pragma regionLoad DLL

    // Method to import a DLL.

    bool UMyTestDll::importDLL(FStringfolder,FString name)

    {

            //这里是通过GamePluginsDir获取当前工程的插件目录,foldernamed都作为参数传递,得到的filePath就是目标dll的具体位置了

            FStringfilePath =*FPaths::GamePluginsDir()+folder+"/"+name

            if (FPaths::FileExists(filePath))

            {

                //通过FPlatformProcess::GetDllHandle获取dll的句柄

                v_dllHandle FPlatformProcess::GetDllHandle(*filePath);//Retrieve the DLL.

                if (v_dllHandle !=NULL)

                {

                    return true;

                }

            }

            return false;  // Return an error.

    }

    #pragma endregionLoad DLL

    #pragma regionImport Methods

    // Imports the method getCircleArea from the DLL.

    bool UMyTestDll::importMethodGetCircleArea()

    {

            if (v_dllHandle !=NULL)

            {

                m_getCircleAreaFromDll =NULL;

                FStringprocName ="getCircleArea";//函数名称.

                 //通过句柄和名称获取到函数指针

                m_getCircleAreaFromDll = (_getCircleArea)FPlatformProcess::GetDllExport(v_dllHandle,*procName);

                if(m_getCircleAreaFromDll !=NULL)

                {

                    return true;

                }

            }

            return false;  // Return an error.

    }

    // dll里面调用对应的函数.

    float UMyTestDll::getCircleAreaFromDll(float radius)

    {

            //如果获取到了这个函数指针

            if (m_getCircleAreaFromDll !=NULL)

            {

                 //通过函数指针调用dll里面的方法,可以在这里断点调试

                float out=float(m_getCircleAreaFromDll(radius));//Call the DLL method with arguments corresponding to the exact signature andreturn type of the method.

                retur nout;

            }

            return    -32202.0F;   //Return an error.

    }

    这一步把dll的加载分成了三步,其实使用的时候需要按照顺序来,先调用importdll导入你需要的dll库,然后调用importMethodGetCircleArea获取到你的函数指针,最后执行getCircleAreaFromDll就可以执行dll库里面的函数了。

    3.dll的拷贝

    创建完下面的类后,需要把你的第三方库的dll拷贝到工程这边,这里我是在MyCClassProject里面新建一个Plugins文件夹,然后里面又新建MyTutorialDLLs文件夹,拷贝到这个文件夹里面。(前面的importDLL(FStringfolder,FStringname)函数,我们可以传入文件夹MyTutorialDLLs以及文件名TestDll)如下图所示

    4.运行与调试

    下面就可以生成工程运行测试了,development版本就可以。运行工程后,新建一个蓝图(继承Actor就行),名为UseDllActor,把蓝图拖进场景,在EventGraph里面书写如下。当然,这些方法,你也可以直接在代码里面调用的。

    在接下来的调试中,你可以在getCircleAreaFromDll方法 这一行floatout=float(m_getCircleAreaFromDll(radius));设置断点。如果F11进入的时候系统提示找不到你dll的源cpp文件,就会弹出下面的对话框,找到你的工程源文件目录就可以了。当然如果你要调试,上面拷贝dll的同时需要把TestDll.pdb文件也拷过来。

    另外,通过动态方式调用dll类的函数是比较麻烦的,大家可以先在网上了解一下,之后有研究的话可能会更新这篇文档。

    五.第三方库PhysX的 dll 调用浅析

            大家可能还是有一点疑惑,引擎里面有很多第三方库dll的调用,难道使用的就是这种方法么?为什么有一些第三方库的build文件里面会有像PublicDelayLoadDLLs这样加载dll的方法,有什么作用?答案是方法大同小异,略有区别,关于PublicDelayLoadDLLs后面再说。下面简单给大家分析一下,UE4中的物理模块——PhysX第三方库的dll的调用。

            首先,我们定位到PhysX模块dll的位置,F:\UnrealEngine4.14\Engine\Binaries\

    ThirdParty\PhysX\Win64\VS2015。(不同平台以及VS版本有不同目录)。我首先就想到,如果把这里的Dll移到其他目录或者删掉会怎么样呢?

    F:\UnrealEngine4.14\Engine\Source\ThirdParty\PhysX\PhysX_3.4\Source是源文件目录)

            果然,提示我找不到PhysX3CommonPROFILE_x64.dll。随后,抛出了个异常,我就打开了调用堆栈。(终于意识到异常的好处了!) 定位到了PhysXLevel.cpp的voidInitGamePhys()函数。异常在268行的位置抛出。

            果断进去看看,定位到了第三方库文件PXFoundation.h。

    PX_C_EXPORT PX_FOUNDATION_API physx::PxFoundation*PX_CALL_CONV

    PxCreateFoundation(physx::PxU32version,physx::PxAllocatorCallback&allocator,physx::PxErrorCallback&errorCallback);

            看着有点眼熟了,你可能说哪里眼熟!?明明看不懂好么。不过你可以仔细看一下这两个宏PX_C_EXPORT 、PX_FOUNDATION_API,跟进去看定义会发现第一个宏就是exturn "C",第二个宏就是__declspec(dllexport)。这不就是我们第一步做的么?

            不过,我们还是有个问题。dll的路径他到底是怎么获取的?我们上面的办法是通过importDll函数搜索到的,那他是不是也应该有个类似的函数呢?没错,就是262行 LoadPhysXMoudles();。再跟进去,熟悉的目录映入眼帘。

            同时,还看到了各种句柄比如PxFoundationHandle,可以看到他的定义就是这样的(具体的使用我就不太清楚了,各位可以自行研究该模块)

            HMODULEPxFoundationHandle = 0;

            DECLARE_HANDLE(HINSTANCE);

            typedefHINSTANCEHMODULE;     /* HMODULEs can be used inplace of HINSTANCEs */

             最后,再说一下Build.cs文件中的PublicDelayLoadDLLs方法。因为网上的教程提到了这两个方法,又没给人解释清,确实让人很烦,而且我上面说了第三方的dll库一般不需要修改build文件,这会让一些朋友有点疑惑。

            其实熟悉windows编程的朋友应该知道,windows中dll的加载有两种方式,一种是在exe运行的时候就加载,而另一种则是在需要用到dll的时候再去加载(可以加快exe启动的速度)。所以PublicDelayLoadDLLs其实就是专门针对第三方库的dll的延迟加载,在这里执行PublicDelayLoadDLLs.Add();实际上就是把这些dll的名称作为参数传递给链接器。

            打开PhysX.build.cs文件,你会看到有好几个dll都是延迟加载的。

    如果想看到编译工具中的这些变量,最简单的方法就是在C#代码里面执行Console.WriteLine(FileName);