首先说明下,这里“跨平台”是指Windows下的32位与64位平台,不涉及windows以外的平台。
以前的项目都是x86平台下运行, 自然也没有考虑过 DotnetFramework 跨多平台解决方案 这个问题。
最近在实现不同平台运行的时候发现有些与我之前想法不符合的地方。
于是便决定记录下来。
平台
dotnetframework 下配置解决方案平台有四种类型:Itanium 、X86、X64、AnyCPU
- Itanium 是Intel“为未来设计的”处理器架构, 当然由于现在还没有到未来,所以就不用考虑它了。
- X86 x86架构的32位系统平台,多是历史原因留下的平台(目前应该很多应用的设备都是基于32位平台的)
- X64 x64架构的64位系统平台,当前流行的平台
- AnyCPU 任意平台,VS默认选项。可以理解为见人说人话,见神说神话。
AnyCPU的优势是显而易见的, 正常纯托管代码都不会出现什么问题,编译一个版本即可在32、64位系统下通用。
不过由于依赖的C/C++等非托管库必须以x86平台编译,这样就无法通用。仅能在x86上运行,
如果需要在64位系统上运行还需要将库编译为x86特定平台,然后将应用程序以32位运行。
而进行互操作的dll文件路径只能是const的, 所以不能通过代码动态给互操作库路径赋值。
那么,若需要同时支持32、64位系统,我们得发布两个版本(x86与x64)
那么是不是在使用互操作的时候,就不能通过AnyCPU的方式生成通用的程序了呢?
跨平台方案
借助搜索引擎没有找到想要的结果, 于是考虑了几种也许可行的方案:
方案一:
思路:设置当前工作目录后加载目录中的指定dll
a. 将本机代码分别编译x86与x64版本。(假设名称为invoke.dll) 然后分别放入运行目录(/x64/invoke.dll /x86/invoke.dll)
b.互操作调用之前使用托管代码Directory.SetCurrentDirectory设置当前的工作目录。
( 通过Environment.Is64BitProcess() 判断平台。 若32位平台,设置成 ./x86/ ; 64位平台设置成./x64/)
c. 调用互操作(试验后发现并不需要每次调用前都设置工作目录,第一次调用设置好即可)
d.编译托管代码为AnyCPU
方案一设置了工作目录后加载目录下的dll是可行的,不过此方法有个致命的缺陷:
在调用Directory.SetCurrentDirectory后,调用互操作之前,工作目录可能被设置到其它位置
这个问题限制了所有互操作的DLL,必须在x86与x64目录中。(或是你取的其它目录名称)
方案二:
思路:使用WIN32 API 直接调用LoadLibrary加载动态库的全路径
a.将本机代码分别编译x86与x64版本。(假设名称为invoke.dll) 然后分别放入运行目录(/x64/invoke.dll /x86/invoke.dll)
b.在调用互操作之前,生成当前本地代码库的全路径名称
例如:在win32下的全路径代码是 D:\test\x86\invoke.dll; 在x64下就是D:\test\x64\invoke.dll
c.调用 WIN32 API LoadLibrary 加载步骤b生成的dll路径
d.调用互操作
e. 调用WIN32 API FreeLibrary 释放c步骤加载的dll
预加载代码如下
private const string LibraryName = "kernel32";
[DllImport(LibraryName, CharSet = CharSet.Auto, BestFitMapping = false, SetLastError = true)]
private static extern IntPtr LoadLibrary(string fileName);
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[DllImport(LibraryName, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FreeLibrary(IntPtr moduleHandle);
/// <summary>
/// 预加载本地dll
/// </summary>
/// <param name="modlueName"></param>
/// <returns></returns>
public static IntPtr PreLoad(string modlueName)
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
string platform = "x86";
if (Environment.Is64BitProcess)
{
platform = "x64";
}
string modPath = string.Format("{0}{1}\\{2}", baseDir, platform, modlueName);
return LoadLibrary(modPath);
}
public static bool FreeLib(IntPtr ptr)
{
return FreeLibrary(ptr);
}
方案二实际上是借助LoadLibrary直接加载动态库到进程中,
该方案是缺点是:每个需要互操作的dll都需要通过PreLaod方法加载下,
而且对于用户控件来说,如果有调用了互操作的控件,需要打开设计视图时候,会报错找不到所需的dll
方案三:
思路:为了突破方案二的局限性,我们得考虑将调用的dll放入系统目录中,并在加载的时候从多个目录中查找
a. 注册dll所在目录到系统的环境变量中
下面的代码功能为:把 CDllPath 加入系统变量,并赋值为dll存放的路径,然后以管理员权限启动
@echo off
rem 将代码保存为bat文件,存放到DLL所在目录,并以管理员权限执行。(这里只测试了win10系统)
%1 mshta vbscript:CreateObject("Shell.Application").ShellExecute("cmd.exe","/c %~s0 ::","","runas",1)(window.close)&&exit
echo 将当前目录添加到环境变量中...【仅测试Win10的环境变量添加】
echo Andwp 2017年3月12日
ver | find "10.0" > NUL && goto win10
set strReg="HKLM\system\controlset\control\session manager\environment"
goto Next
:win10
@echo Win10系统特殊处理
set strReg="HKLM\system\ControlSet001\control\session manager\environment"
goto Next
:Next
set strDir=%~dp0
echo 获取到当前目录路径 %strDir%
reg add %strReg% /v CDllPath /t REG_SZ /d %strDir%
echo 环境变量添加成功!
pause
b.C#调用C/C++动态库之前,在默认路径中查找dll路径,加载成功才返回。
代码如下:
/// <summary>
/// 预加载本地dll
/// </summary>
/// <param name="modlueName"></param>
/// <param name="loadPath">加载路径</param>
/// <returns></returns>
public static IntPtr PreLoad(string modlueName, ref string loadPath)
{
List<string> baseDir = new List<string>();
baseDir.Add(AppDomain.CurrentDomain.BaseDirectory);
// 从系统环境变量加载
string evnPath = System.Environment.GetEnvironmentVariable("CDllPath");
string[] evns = evnPath.Split(';');
baseDir.AddRange(evns);
string platform = "x86";
if (Environment.Is64BitProcess)
{
platform = "x64";
}
IntPtr ptr = IntPtr.Zero;
loadPath = string.Empty;
for (int i = 0; i < baseDir.Count; i++)
{
if (!string.IsNullOrEmpty(baseDir[i]))
{
string strPath = string.Format("{0}\\{1}\\{2}", baseDir[i].TrimEnd('\\'), platform, modlueName);
ptr = LoadLibrary(strPath);
if (ptr != IntPtr.Zero)
{
loadPath = strPath;
break;
}
loadPath += strPath;
}
}
return ptr;
}
这里仅查找了a步骤中的导入的目录与当前目录下的x64与x86,若有更多的要添加,可以在此思路上扩展。
方法四:
windowsAPI提供了函数SetDllDirectory,设置动态库的搜索路径,这就比较方便,是什么平台就加载什么目录,具体使用可以自己去了解下。https://docs.microsoft.com/zh-cn/windows/desktop/api/winbase/nf-winbase-setdlldirectorya
结束语
本文仅仅介绍一些互操作跨平台的思路,并不是标准答案或最佳答案。
我相信一定还有更多的可行方案,在此抛砖引玉,欢迎各位提出。