返回「计算机、信息技术与工程」

UE二进制插件兼容问题及其延申

UE二进制插件兼容问题及其延申 Unreal Engine加载模块时的操作 ![](assets/image 20230817145524277.png)

更多
Markdown 结构化数据
本文目录 13 个章节

UE二进制插件兼容问题及其延申

Unreal Engine加载模块时的操作

assets/image-20230817145524277.png

  1. 查找插件的描述文件:UE会首先查找插件的描述文件(通常是.uplugin文件)以确定插件的基本信息、模块、依赖项等。
  2. 加载模块的二进制文件:如果模块是编译好的,引擎会试图加载相应的二进制文件,如DLLs(Windows系统下)。
  3. 处理依赖关系:如果您的插件依赖其他模块或插件,引擎会确保这些依赖项首先被加载。
  4. 运行模块的启动代码:在模块的加载过程中,特定的启动代码会被执行,如StartupModule方法。

操作系统加载DLL时的操作

assets/image-20230817151155092.png 当操作系统加载C++编译后的DLL(动态链接库)时,它会执行以下操作:

  1. 定位DLL文件:首先,操作系统需要找到DLL文件。这通常是基于应用程序的请求或系统的搜索路径来完成的。
  2. 读取DLL的头部信息:操作系统读取DLL文件的头部信息,这部分信息描述了DLL的结构和其它元数据。
  3. 映射到内存:DLL文件被映射到进程的地址空间。操作系统通常使用内存映射技术将DLL的内容加载到适当的内存区域。
  4. 处理重定位:如果DLL不能被加载到它所请求的默认基地址(由于该地址已被其他内容占用),那么加载器必须对其内部的地址进行重定位。
  5. 解析导入表:DLL通常会使用或者依赖其他DLL。这些依赖关系会在导入表中列出。加载器会解析这个导入表,并确保所有列出的DLL都已加载。如果这些依赖的DLL还没有被加载到内存,加载器会递归地加载它们。
  6. 初始化静态/全局变量:加载器会为DLL内部的静态或全局变量分配内存并执行初始化。
  7. 调用DLL的入口点:大多数DLL都有一个称为DllMain的特定函数,它在DLL被加载或卸载时被调用。加载器会在适当的时机调用这个函数。
  8. 链接到应用程序:一旦DLL被加载和初始化,应用程序就可以开始调用它提供的函数和方法了。
  9. 处理异常和错误:如果在上述过程中出现任何问题(例如找不到DLL、解析导入表时缺少依赖项等),操作系统会产生相应的错误或异常。

需要注意的是,上述步骤是一个简化的、高级的概述。实际的加载过程可能涉及更多细节和特定于操作系统的操作。

二进制DLL不兼容的可能原因

assets/image-20230817150610140.png

  1. API不兼容:当一个库或程序的应用程序接口(API)发生变化并且没有向后兼容旧版本时,使用这个库的程序可能会因为找不到预期的函数或方法而出现错误。
  2. ABI不兼容:应用程序二进制接口(ABI)描述了程序之间如何在二进制级别上进行交互。当库的ABI发生变化时,即使API没有变化,使用这个库的程序也可能会出现错误。
  3. 平台不兼容:不同的操作系统或架构可能需要不同的二进制格式。例如,为Windows编译的程序不能在Linux上运行,为x86架构编译的程序不能在ARM上运行。
  4. 依赖库版本不兼容:如果一个程序依赖于特定版本的库,并且该库的新版本不向后兼容,那么程序可能无法正常工作。

操作系统环境层面的原因:

  1. 运行时库版本:有些.dll文件需要特定版本的Visual C++ 运行时库。如果操作系统上没有安装正确的版本,加载可能会失败。
  2. API版本不匹配:如果.dll文件使用了仅在特定Windows版本上可用的API,那么在其他版本上加载它可能会出现问题。
  3. 32位与64位冲突:如果你尝试在64位操作系统上加载32位.dll(或反之),这可能会导致问题,除非应用程序也是相应地设置为32位或64位。
  4. 系统级别的安全策略:某些Windows安全设置可能会阻止从特定位置加载.dll文件,特别是如果它们被认为是不受信任的。
  5. 已知的系统缺陷或错误:某些操作系统版本可能存在特定的问题或错误,这可能会影响到.dll文件的加载。
  6. 系统组件版本.dll文件可能依赖于操作系统的特定组件版本。例如,一个针对Windows 10的.dll可能依赖于某个特定版本的Windows组件,而这在Windows 7上可能不可用。

UE插件不兼容问题与解决方案

Q: 在Unreal Engine插件开发中,当我们不提供源代码版本,如何确保发布的二进制模块与用户自定义引擎版本的兼容性?

不兼容的表现有哪些

assets/image-20230817144644090.png

  1. 启动崩溃:引擎可能在启动时立即崩溃。
  2. 运行时错误:在引擎运行过程中,当访问或使用插件功能时,可能会遇到错误或崩溃。
  3. 功能缺失或不正常:即使插件似乎在加载和运行时没有问题,某些功能可能无法正常工作或完全缺失。
  4. 编译问题:当尝试编译项目时,可能会出现编译错误。
  5. 版本冲突的警告:Unreal Engine可能会在启动或加载项目时显示版本冲突的警告。

不兼容可能的原因

引擎层面: assets/image-20230817144723362.png

引擎层面

  1. 引擎API的变更:当一个库或程序的应用程序接口(API)发生变化并且没有向后兼容旧版本时,使用这个库的程序可能会因为找不到预期的函数或方法而出现错误。每个Unreal Engine版本都可能包含API的改动。如果插件使用了已经被弃用或修改的API,那么插件就可能不兼容。

  2. 引擎内部结构或数据格式变化:随着引擎的更新和优化,内部结构和数据格式可能会发生变化。

  3. 依赖的问题:插件可能依赖于某些特定的外部库或其他插件,而这些依赖项可能与新版本的引擎不兼容。

  4. 编译器和工具链的变化:新版本的引擎可能使用了新的编译器或工具链,这可能与二进制插件不兼容。 确保插件是为你当前使用的UE版本和平台编译的。如果你正在使用自编译的引擎,确保插件也是在相同的环境下编译的。

  5. 缺少依赖项:使用依赖检查工具,如Windows上的Dependency Walker,检查.dll文件是否缺少任何依赖。

  6. 链接问题:如果用户修改了引擎中的某些部分,特别是UwaProjScan依赖的部分,那么可能会导致链接问题或运行时错误。

系统层面:

  1. ABI不兼容:应用程序二进制接口(ABI)描述了程序之间如何在二进制级别上进行交互。当库的ABI发生变化时,即使API没有变化,使用这个库的程序也可能会出现错误。
  2. 平台不兼容:不同的操作系统或架构可能需要不同的二进制格式。例如,为Windows编译的程序不能在Linux上运行,为x86架构编译的程序不能在ARM上运行。
  3. 依赖库版本不兼容:如果一个程序依赖于特定版本的库,并且该库的新版本不向后兼容,那么程序可能无法正常工作。

在所依赖DLL都存在的情况下,不兼容的原因 assets/image-20230817153226614.png

240508 GPT4补充

Q:UE引擎的资源检测的插件是第三方使用官方的引擎预编译好的。但是用户的引擎是自己通过编译的。这种情况就是无法保证兼容的对吗?技术原理是什么? 我的理解是,预编译好的插件,与非同一环境预编译好的引擎,在插件调用所依赖的引擎接口的时候,是有可能出现地址对不上的情况的

A:您的理解是正确的。在不同的环境中编译的引擎和插件之间确实可能存在兼容性问题,尤其是当使用第三方预编译的插件而用户使用自己编译的引擎时。以下是一些技术原理和详细解释,为什么会出现这种情况:

  1. 编译环境差异
    • 编译环境包括编译器版本、编译器设置、操作系统版本以及其他相关库的版本。这些因素都会影响最终生成的二进制文件。如果预编译的插件和用户自己编译的引擎是在不同的编译环境中生成的,二者之间可能会出现兼容性问题。
  2. ABI(应用二进制接口)不兼容
    • 应用二进制接口(ABI)定义了程序二进制文件与操作系统之间的接口,包括函数调用约定、系统调用约定等。不同版本的编译器和标准库可能会导致ABI的变化,从而导致预编译的插件无法正确调用用户编译的引擎接口。
  3. 内存地址和符号解析
    • 在编译过程中,编译器会将符号(例如函数和变量)映射到内存地址。如果预编译的插件和用户编译的引擎在不同环境下生成,其符号表和内存地址可能会不匹配,这会导致插件在调用引擎接口时找不到正确的地址或函数实现,最终导致运行时错误或崩溃。
  4. 依赖项和库版本
    • 预编译的插件可能依赖特定版本的动态链接库(DLL或.so文件)。如果用户的引擎编译环境中使用了不同版本的库,这些库的接口或实现可能有所不同,从而导致兼容性问题。

1. ABI简介

ABI(应用二进制接口)定义了程序在二进制级别如何与操作系统和其他程序交互。它包括:

  • 函数调用约定:如何传递函数参数,如何返回值。
  • 数据类型的大小和对齐方式
  • 系统调用的方式。 不同的编译器或不同的编译器版本可能会有不同的ABI,即使源代码相同,生成的二进制文件可能不兼容。

2. 函数调用约定

函数调用约定定义了函数如何接收参数和返回值,具体包括:

  • 参数传递顺序(左到右或右到左)。
  • 使用哪些寄存器传递参数。
  • 使用堆栈传递参数的顺序。
  • 返回值存储在哪些寄存器中。

4. 内存布局和对齐

不同的ABI可能对数据结构的内存布局和对齐方式有不同的规定。例如,结构体在内存中的布局可能不同,导致同一个结构体在不同编译器中生成的二进制格式不同。

5. 堆栈管理

函数调用过程中,堆栈的使用方式也可能不同。例如,某些ABI要求函数调用者负责清理堆栈,而其他ABI可能要求被调用的函数负责清理堆栈。这些差异会导致函数返回后堆栈状态不一致,进而导致程序崩溃。

示例 - 不同的编译器约定

假设有两种不同的函数调用约定:约定A和约定B。

  • 约定A:第一个参数使用寄存器eax传递,第二个参数使用寄存器ebx传递,返回值使用寄存器eax
  • 约定B:第一个参数使用寄存器ecx传递,第二个参数使用寄存器edx传递,返回值使用寄存器eax

考虑一个简单的C函数:

int add(int a, int b) {
    return a + b;
}

编译器1(使用约定A)生成的汇编代码:

add:
    mov eax, [esp+4]  ; 将第一个参数(a)从堆栈加载到eax
    add eax, [esp+8]  ; 将第二个参数(b)从堆栈加载到eax
    ret

编译器2(使用约定B)生成的汇编代码:

add:
    mov eax, ecx  ; 将第一个参数(a)从ecx加载到eax
    add eax, edx  ; 将第二个参数(b)从edx加载到eax
    ret

假设我们用编译器1编译了一个插件,而用户用编译器2编译了引擎。插件调用引擎中的add函数时:

  • 插件会按照约定A将参数传递给函数:第一个参数传递给eax,第二个参数传递给ebx
  • 引擎中的函数会按照约定B接收参数:从ecxedx接收参数。 这会导致什么问题?
  • 插件把参数放到了eaxebx中,而引擎期望在ecxedx中找到参数。
  • 结果,函数add在引擎中执行时,会使用错误的参数,导致不正确的返回值。

不带源码发布遇到兼容问题的解决方案

assets/image-20230817144746726.png

提供兼容层: 开发一个兼容层或者中间件,用于在插件和引擎之间建立一个桥梁,确保核心功能的兼容性。这需要深入了解UE的内部工作原理,并可能涉及更多的开发工作。

明确文档说明: 在插件的官方文档或FAQ中明确说明哪些UE版本是支持的,以及在自编译的引擎版本上可能遇到的问题。这样用户至少在购买或下载插件前可以了解潜在的风险。

考虑源码授权:虽然可能出于多种原因不希望公开源代码,但考虑为特定的大客户或合作伙伴提供源码访问权限也是一个方案。他们可以根据自己的需要自行编译和修改插件。