生成 Arm64X 二进制文件

你可以生成 Arm64X 二进制文件(也称为 Arm64X PE 文件),以支持将单个二进制文件加载到 x64/Arm64EC 和 Arm64 进程。

从 Visual Studio 项目生成 Arm64X 二进制文件

若要启用生成 Arm64X 二进制文件,Arm64EC 配置的属性页新增了“生成项目作为 ARM64X”属性,即项目文件中的 BuildAsX

Arm64EC 配置的属性页,其中显示“生成项目为 ARM64X”选项

当用户生成项目时,Visual Studio 通常会针对 Arm64EC 进行编译,然后将输出链接到 Arm64EC 二进制文件。 当 BuildAsX 别设置为 true 时,Visual Studio 将改为同时为 Arm64EC Arm64 进行编译。 然后,使用 Arm64EC 链接步骤将两者同时链接到一个 Arm64X 二进制文件。 Arm64X 二进制文件的输出目录将与 Arm64EC 配置中设置的输出目录相同。

若要让 BuildAsX 正常工作,除了 Arm64EC 配置之外,用户还必须拥有现有的 Arm64 配置。 Arm64 和 Arm64EC 配置必须具有相同的 C 运行时和 C++ 标准库(例如,同时设置 /MT)。 为了避免生成效率低下的问题,例如生成完整的 Arm64 项目,而不仅仅是编译,项目的所有直接和间接引用都应将 BuildAsX 设置为 true。

生成系统假定 Arm64 和 Arm64EC 配置具有相同的名称。 如果 Arm64 和 Arm64EC 配置的名称不同(例如 Debug|ARM64MyDebug|ARM64EC)你可以手动编辑 vcxprojDirectory.Build.props 文件,以便为提供 Arm64 配置名称的 Arm64EC 配置添加 ARM64ConfigurationNameForX 属性。

如果所需的 Arm64X 二进制文件是两个独立项目(一个是 Arm64 项目,另一个是 Arm64EC 项目)的组合,则可以手动编辑 Arm64EC 项目的 vxcproj,以便添加 ARM64ProjectForX 属性并指定 Arm64 项目的路径。 这两个项目必须在同一个解决方案中。

使用 CMake 生成 Arm64X DLL

若要将 CMake 项目二进制文件生成为 Arm64X,可以使用支持生成为 Arm64EC 的任何 CMake 版本。 此过程涉及最初生成面向 Arm64 的项目以生成 Arm64 链接器输入。 随后,应再次生成面向 Arm64EC 的项目,这次将 Arm64 和 Arm64EC 输入组合在一起,形成 Arm64X 二进制文件。 以下步骤利用CMakePresets.json的使用

  1. 确保具有面向 Arm64 和 Arm64EC 的单独配置预设。 例如:

     {
       "version": 3,
       "configurePresets": [
         {
           "name": "windows-base",
           "hidden": true,
           "binaryDir": "${sourceDir}/out/build/${presetName}",
           "installDir": "${sourceDir}/out/install/${presetName}",
           "cacheVariables": {
             "CMAKE_C_COMPILER": "cl.exe",
             "CMAKE_CXX_COMPILER": "cl.exe"
           },
     	  "generator": "Visual Studio 17 2022",
         },
         {
           "name": "arm64-debug",
           "displayName": "arm64 Debug",
           "inherits": "windows-base",
           "hidden": true,
     	  "architecture": {
     		 "value": "arm64",
     		 "strategy": "set"
     	  },
           "cacheVariables": {
             "CMAKE_BUILD_TYPE": "Debug"
           }
         },
         {
           "name": "arm64ec-debug",
           "displayName": "arm64ec Debug",
           "inherits": "windows-base",
           "hidden": true,
           "architecture": {
             "value": "arm64ec",
             "strategy": "set"
           },
           "cacheVariables": {
             "CMAKE_BUILD_TYPE": "Debug"
           }
         }
       ]
     }
    
  2. 添加两个新配置,这些配置继承自上述 Arm64 和 Arm64EC 预设。 在继承自 Arm64EC 和ARM64BUILD_AS_ARM64X另一个配置的配置中设置为BUILD_AS_ARM64XARM64EC。 这些变量将用于表示这两个预设中的生成是 Arm64X 的一部分。

         {
           "name": "arm64-debug-x",
           "displayName": "arm64 Debug (arm64x)",
           "inherits": "arm64-debug",
           "cacheVariables": {
             "BUILD_AS_ARM64X": "ARM64"
         },
      	{
           "name": "arm64ec-debug-x",
           "displayName": "arm64ec Debug (arm64x)",
           "inherits": "arm64ec-debug",
           "cacheVariables": {
             "BUILD_AS_ARM64X": "ARM64EC"
         }
    
  3. 将新的 .cmake 文件添加到名为 arm64x.cmake 的 CMake 项目。 将以下代码片段复制到新的 .cmake 文件中。

     # directory where the link.rsp file generated during arm64 build will be stored
     set(arm64ReproDir "${CMAKE_CURRENT_SOURCE_DIR}/repros")
    
     # This function reads in the content of the rsp file outputted from arm64 build for a target. Then passes the arm64 libs, objs and def file to the linker using /machine:arm64x to combine them with the arm64ec counterparts and create an arm64x binary.
    
     function(set_arm64_dependencies n)
     	set(REPRO_FILE "${arm64ReproDir}/${n}.rsp")
     	file(STRINGS "${REPRO_FILE}" ARM64_OBJS REGEX obj\"$)
     	file(STRINGS "${REPRO_FILE}" ARM64_DEF REGEX def\"$)
     	file(STRINGS "${REPRO_FILE}" ARM64_LIBS REGEX lib\"$)
     	string(REPLACE "\"" ";" ARM64_OBJS "${ARM64_OBJS}")
     	string(REPLACE "\"" ";" ARM64_LIBS "${ARM64_LIBS}")
     	string(REPLACE "\"" ";" ARM64_DEF "${ARM64_DEF}")
     	string(REPLACE "/def:" "/defArm64Native:" ARM64_DEF "${ARM64_DEF}")
    
     	target_sources(${n} PRIVATE ${ARM64_OBJS})
     	target_link_options(${n} PRIVATE /machine:arm64x "${ARM64_DEF}" "${ARM64_LIBS}")
     endfunction()
    
     # During the arm64 build, create link.rsp files that containes the absolute path to the inputs passed to the linker (objs, def files, libs).
    
     if("${BUILD_AS_ARM64X}" STREQUAL "ARM64")
     	add_custom_target(mkdirs ALL COMMAND cmd /c (if not exist \"${arm64ReproDir}/\" mkdir \"${arm64ReproDir}\" ))
     	foreach (n ${ARM64X_TARGETS})
     		add_dependencies(${n} mkdirs)
     		# tell the linker to produce this special rsp file that has absolute paths to its inputs
     		target_link_options(${n} PRIVATE "/LINKREPROFULLPATHRSP:${arm64ReproDir}/${n}.rsp")
     	endforeach()
    
     # During the ARM64EC build, modify the link step appropriately to produce an arm64x binary
     elseif("${BUILD_AS_ARM64X}" STREQUAL "ARM64EC")
     	foreach (n ${ARM64X_TARGETS})
     		set_arm64_dependencies(${n})
     	endforeach()
     endif()
    

仅当使用 Visual Studio 17.11 或更高版本中的 MSVC 链接器进行生成时,才支持 /LINKREPROFULLPATHRSP

如果需要使用较旧的链接器,请改为复制以下代码片段。 此路由使用较旧的标志 /LINK_REPRO。 使用 /LINK_REPRO 路由会导致总体生成时间变慢,因为复制文件,在使用 Ninja 生成器时存在已知问题。

# directory where the link_repro directories for each arm64x target will be created during arm64 build.
set(arm64ReproDir "${CMAKE_CURRENT_SOURCE_DIR}/repros")

# This function globs the linker input files that was copied into a repro_directory for each target during arm64 build. Then it passes the arm64 libs, objs and def file to the linker using /machine:arm64x to combine them with the arm64ec counterparts and create an arm64x binary.

function(set_arm64_dependencies n)
	set(ARM64_LIBS)
	set(ARM64_OBJS)
	set(ARM64_DEF)
	set(REPRO_PATH "${arm64ReproDir}/${n}")
	if(NOT EXISTS "${REPRO_PATH}")
		set(REPRO_PATH "${arm64ReproDir}/${n}_temp")
	endif()
	file(GLOB ARM64_OBJS "${REPRO_PATH}/*.obj")
	file(GLOB ARM64_DEF "${REPRO_PATH}/*.def")
	file(GLOB ARM64_LIBS "${REPRO_PATH}/*.LIB")

	if(NOT "${ARM64_DEF}" STREQUAL "")
		set(ARM64_DEF "/defArm64Native:${ARM64_DEF}")
	endif()
	target_sources(${n} PRIVATE ${ARM64_OBJS})
	target_link_options(${n} PRIVATE /machine:arm64x "${ARM64_DEF}" "${ARM64_LIBS}")
endfunction()

# During the arm64 build, pass the /link_repro flag to linker so it knows to copy into a directory, all the file inputs needed by the linker for arm64 build (objs, def files, libs).
# extra logic added to deal with rebuilds and avoiding overwriting directories.
if("${BUILD_AS_ARM64X}" STREQUAL "ARM64")
	foreach (n ${ARM64X_TARGETS})
		add_custom_target(mkdirs_${n} ALL COMMAND cmd /c (if exist \"${arm64ReproDir}/${n}_temp/\" rmdir /s /q \"${arm64ReproDir}/${n}_temp\") && mkdir \"${arm64ReproDir}/${n}_temp\" )
		add_dependencies(${n} mkdirs_${n})
		target_link_options(${n} PRIVATE "/LINKREPRO:${arm64ReproDir}/${n}_temp")
		add_custom_target(${n}_checkRepro ALL COMMAND cmd /c if exist \"${n}_temp/*.obj\" if exist \"${n}\" rmdir /s /q \"${n}\" 2>nul && if not exist \"${n}\" ren \"${n}_temp\" \"${n}\" WORKING_DIRECTORY ${arm64ReproDir})
		add_dependencies(${n}_checkRepro ${n})
	endforeach()

# During the ARM64EC build, modify the link step appropriately to produce an arm64x binary
elseif("${BUILD_AS_ARM64X}" STREQUAL "ARM64EC")
	foreach (n ${ARM64X_TARGETS})
		set_arm64_dependencies(${n})
	endforeach()
endif()
  1. 在项目中顶级 CMakeLists.txt 文件的底部,添加以下代码片段。 请务必将尖括号的内容替换为实际值。 这将使用 arm64x.cmake 前面刚刚创建的文件。

     if(DEFINED BUILD_AS_ARM64X)
     	set(ARM64X_TARGETS <Targets you want to Build as ARM64X>)
     	include("<directory location of the arm64x.cmake file>/arm64x.cmake")
     endif()
    
  2. 使用已启用 Arm64X 的 Arm64 预设(arm64-debug-x)生成 CMake 项目。

  3. 使用已启用 Arm64X 的 Arm64EC 预设(arm64ec-debug-x)生成 CMake 项目。 此生成的输出目录中包含的最终 dll(s)将是 Arm64X 二进制文件。

生成 Arm64X 纯转发器 DLL

Arm64X 纯转发器 DLL 是一个小型 Arm64X DLL,它会根据 API 的类型将 API 转发到单独的 DLL:

  • Arm64 API 将转发到 Arm64 DLL。

  • x64 API 会被转发到 x64 或 Arm64EC DLL。

即使在生成包含所有 Arm64EC 和 Arm64 代码的合并 Arm64X 二进制文件时遇到困难,Arm64X 纯转发器也能发挥使用 Arm64X 二进制文件的优势。 在 Arm64X PE 文件概述页面了解有关 Arm64X 纯转发器 DLL 的更多信息。

你可以按照以下步骤从 Arm64 开发人员命令提示符生成 Arm64X 纯转发器。 生成的 Arm64X 纯转发器会将 x64 调用路由至 foo_x64.DLL,并将 Arm64 调用路由至 foo_arm64.DLL

  1. 创建稍后供链接器用来创建纯转发器的空 OBJ 文件。 这些文件都是空的,因为纯转发器中没有代码。 为此,请创建一个空文件。 对于以下示例,我们将文件命名为 empty.cpp。 然后使用 cl 来创建空 OBJ 文件,其中一个用于 Arm64 (empty_arm64.obj),而另一个用于 Arm64EC (empty_x64.obj):

    cl /c /Foempty_arm64.obj empty.cpp
    cl /c /arm64EC /Foempty_x64.obj empty.cpp
    

    如果出现错误消息“cl : Command line warning D9002 : ignoring unknown option '-arm64EC'”,则说明使用的编译器不正确。 若要解决此问题,请切换到 Arm64 开发人员命令提示符

  2. 为 x64 和 Arm64 创建 DEF 文件。 这些文件枚举了 DLL 的所有 API 导出,并将加载程序指向可实现这些 API 调用的 DLL 的名称。

    foo_x64.def:

    EXPORTS
        MyAPI1  =  foo_x64.MyAPI1
        MyAPI2  =  foo_x64.MyAPI2
    

    foo_arm64.def:

    EXPORTS
        MyAPI1  =  foo_arm64.MyAPI1
        MyAPI2  =  foo_arm64.MyAPI2
    
  3. 然后,你可以使用 link 来为 x64 和 Arm64 创建 LIB 导入文件:

    link /lib /machine:x64 /def:foo_x64.def /out:foo_x64.lib
    link /lib /machine:arm64 /def:foo_arm64.def /out:foo_arm64.lib
    
  4. 链接空的 OBJ 并使用标志 /MACHINE:ARM64X 导入 LIB 文件,以便生成 Arm6X 纯转发器 DLL:

    link /dll /noentry /machine:arm64x /defArm64Native:foo_arm64.def /def:foo_x64.def empty_arm64.obj empty_x64.obj /out:foo.dll foo_arm64.lib foo_x64.lib
    

最终的 foo.dll 可以被加载到 Arm64 或 x64/Arm64EC 进程中。 当 Arm64 进程加载 foo.dll 时,操作系统将立即加载 foo_arm64.dll 到其位置,而任何 API 调用都将由 foo_arm64.dll 处理。