C# 使用 "3f/DllExport" 工具导出C风格的本机函数
[文 / 张赐荣]
首先,让我们来了解一下什么是争渡读屏软件,以及什么是争渡文本预处理API。争渡读屏软件是一款屏幕朗读软件,用于协助视力障碍人士操作电脑。
争渡文本预处理API是一种让插件可以修改读屏朗读的文本的接口,它是一个32位的动态链接库(DLL),文件名为ZDTextPreprocess.dll,放在"争渡读屏安装目录\addins"下。读屏会优先加载这个DLL文件,然后调用其中的两个函数:Init和TextPreprocess。
"Init"函数是用来初始化插件的,它没有参数,只返回一个整数值,表示当前插件的版本号。这个版本号应该和文档中的版本号一致,目前是1。
"TextPreprocess"函数是用来对文本进行预处理的,它有两个参数:oldString和newString。oldString是一个宽字符指针,指向读屏即将朗读的原始文本。newString也是一个宽字符指针,指向插件修改后的新文本,它的缓冲区大小为40960字节,如果超出了这个大小,需要截断多余的部分。这个函数返回一个整数值,表示新文本的长度。如果发生了错误,或者没有替换文本,就返回0。
插件开发者可以根据自己的需求,编写TextPreprocess函数的逻辑,实现对读屏朗读文本的定制化修改。例如,可以替换一些特殊符号,或者添加一些注释等。
接下来,让我们来看看如何用C#来编写争渡文本预处理插件。我们需要使用一个叫"3f/DllExport"的工具,它可以让.NET程序集导出本机风格的函数。这样我们就可以用C#来编写类似于C语言生成的DLL一样的插件了。
"3f/DllExport"是一个开源项目,在GitHub上可以找到它的源代码和文档。它是在"Unmanaged Exports"基础上发展而来的。"Unmanaged Exports"是一个早期的尝试,它也可以让.NET程序集导出函数,但是它有很多问题,而且已经很久没有更新了。
"3f/DllExport"的工作原理是修改了.NET生成的DLL,在其中插入了本机DLL的头以及导出函数表、重定位函数表信息等。这样,它就像一个普通的本机DLL一样,可以被任何支持调用本机DLL的语言或平台使用。当然,它本质上并不是真正的本机DLL,它的执行代码依然是IL(中间语言)而不是本机二进制代码,所以本身还是要依赖.NET运行时。只是因为现在有了导出函数表,并且加入了类似于普通DLL的头信息,看起来就与普通C语言导出的DLL相似。
这与最新的.NET 7 Native AOT不一样,那个是真正地编译为了非托管本机代码,可以脱离.NET运行时。但是.NET 7 Native AOT不支持X86(32位),而易语言只支持X86的DLL。所以对于我们来说,并没有什么用处。
我不知道为什么微软官方没有去做这样的支持,反而还需要第三方工具来修改。
不过,既然有了这样一个好用的工具,就不用再抱怨了。我们可以利用它来实现我们想要的功能,而不用再去学习和使用C++或其他语言。可以用C#的强大功能和丰富的库,来编写更多的文本预处理效果。
那么"3f/DllExport"怎么使用呢?下面我就以编写争渡文本预处理插件为例,给大家做一个简单的教程。
首先,我们需要下载"3f/DllExport"这个工具。我们可以前往它的GitHub页面,找到它的最新版本,目前是"v1.7.4 (Latest on Jan 3, 2021)",文件名应该是"offline.DllExport.1.7.4.29858.c1cc52f.zip"。我们下载并解压这个文件,可以看到里面有很多文件和文件夹。
接着,我们需要打开Visual Studio 2019,新建一个.NET Framework类库项目,名字叫"TextPreprocessAddon"。然后关闭Visual Studio,将刚才解压的文件全部复制到我们的解决方案文件夹下(也就是与*.sln同目录的文件夹)。然后运行目录中的"DllExport.bat"进行配置。打开后有很多配置和选项,我们暂时不用管,勾选"Installed"复选框,然后点击"Apply"应用设置。它会修改我们的项目配置文件,让它支持导出函数。
配置DllExport就基本完成了,接下去就是编写代码了。导出本机函数的方法是在代码中,使用[DllExport]特性来标记静态方法。"DllExport"特性支持自定义导出函数的名字、调用约定等。与["DllImport"]特性有些类似。注意,如果不指定调用约定,默认导出的是"__cdecl",由于Windows API和易语言只支持stdcall方式导出,所以调用约定我们选择"WINAPI"。DllExport也支持"MarshalAs"特性,可以用这个特性告诉.NET如何将.NET托管类型转换为本机非托管类型。
C#代码:
----------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace TextPreprocessAddon
{
public static class TextPreprocessAddon
{
[DllExport("Init",CallingConvention.Winapi)]
public static int Init ()
{
return (1);
}
[DllExport("TextPreprocess",CallingConvention.Winapi)]
public static int TextPreprocess (IntPtr oldTxtPtr,IntPtr newTxtPtr)
{
try
{
string oldText = Marshal.PtrToStringUni(oldTxtPtr);
string newText = oldText.Replace("女孩","男孩");
byte[] tempBytes = Encoding.Unicode.GetBytes(newText+"\0");
Marshal.Copy(tempBytes,0,newTxtPtr,tempBytes.Length);
return (newText.Length);
}
catch (Exception ex)
{
return (0);
}
}
}
}
----------
上面的代码是用C#编写的争渡文本预处理插件,它可以将原始文本中的"女孩"替换成"男孩"。导出函数的实现过程如下:
使用[DllExport]特性来标记静态方法,指定导出函数的名称和调用约定。
使用Marshal类中的方法来转换指针和字符串类型,以及复制字节数组到内存空间。
使用字符串类型中的方法来替换文本中的内容,以及添加空字符。
返回新文本的长度,或者在发生错误时返回0。
然后编译这个项目,可以在"release"文件夹中找到X86的文件夹,进去就有导出的DLL了。
这样,我们就用C#编写了一个争渡文本预处理插件,它可以将原始文本中的"女孩"替换成"男孩",并将修改后的文本送给读屏朗读。我们可以将这个DLL文件放在争渡读屏安装目录\addins\下,然后启动争渡读屏软件,选择我们的插件,就可以听到效果了。
使用Python调用上面导出的DLL,实现Python调用C#代码。
import ctypes
# 加载 dll(完整路径)
my_lib = ctypes.WinDLL('d:\TextPreprocessAddon.dll')
# 定义函数原型
my_lib.Init.restype = ctypes.c_int
# 调用函数
int_result = my_lib.Init()
print("Result:", int_result)
# 调用 TextPreprocess 函数,并获取返回值
old_string = "这个小女孩长得很漂亮。"
new_string = ctypes.create_unicode_buffer(65536) # 创建一个 Unicode 缓冲区
result = my_lib.TextPreprocess(ctypes.c_wchar_p(old_string), new_string)
if result > 0:
print("New String:", new_string.value)
else:
print("Old String:", old_string)
input()
首先,我们需要导入ctypes模块,它是Python的一个标准库,可以让我们调用本机DLL中的函数。
然后,使用ctypes.WinDLL函数,加载我们的DLL文件,注意要指定完整的路径。这个函数会返回一个对象,我们把它赋值给my_lib变量,以便后面使用。
接着,使用my_lib.Init.restype属性,定义Init函数的返回类型为整数类型。这样我们就可以正确地接收Init函数的返回值了。
然后,直接调用my_lib.Init()函数,它会返回一个整数值,表示当前插件的版本号。我们把它赋值给int_result变量,并打印出来。
接着,我们准备调用TextPreprocess函数,它有两个参数:oldTxtPtr和newTxtPtr。oldTxtPtr是一个指向读屏即将朗读的原始文本的指针,newTxtPtr是一个指向插件修改后的新文本的指针。
为了传递这两个参数,我们需要做一些转换。首先,我们定义一个字符串变量old_string,赋值为"这个小女孩长得很漂亮。"。这是我们想要修改的原始文本。
然后,使用ctypes.c_wchar_p函数,将old_string转换为一个宽字符指针类型的对象。这样就可以作为oldTxtPtr参数传递给TextPreprocess函数了。
接着,我们使用ctypes.create_unicode_buffer函数,创建一个Unicode缓冲区类型的对象。这个函数需要一个参数,表示缓冲区的大小。我们给它传递65536,表示缓冲区可以存储65536个字节。这个缓冲区就可以作为newTxtPtr参数传递给TextPreprocess函数了。
然后,调用my_lib.TextPreprocess函数,并传递oldTxtPtr和newTxtPtr两个参数。这个函数会返回一个整数值,表示新文本的长度。我们把它赋值给result变量,并判断是否大于0。
如果result大于0,表示插件成功地修改了文本,并将新文本存储在newTxtPtr指向的缓冲区中。我们可以使用new_string.value属性,获取缓冲区中的字符串值,并打印出来。
如果result等于0,表示插件没有修改文本,或者发生了错误。我们就直接打印old_string变量的值。
通过这段Python代码,我们就可以看到争渡文本预处理插件的效果了。它会将原始文本中的"女孩"替换成"男孩",并将修改后的文本送给读屏朗读。
你看,用C#导出本机DLL给Python是多么美好的事情啊!我们不仅可以利用C#的强大功能和丰富的库来编写更多的文本预处理效果,还可以让Python等其他语言方便地调用我们的插件。这样就实现了跨语言和跨平台的互操作性。
这只是一个简单的导出函数例子。
"3f/DllExport"的最大优点是可以让C#也能导出本机风格的函数,供其他语言或平台调用,从而实现跨语言和跨平台的互操作性。这个库的出现,解决了C#的一大短板,就是不能像C++一样生成普通的动态链接库。
"3f/DllExport"工具非常强大,支持与"ilmerge"配合使用,如果你的程序集引用了第三方库,它可以配合ILMerge将其合并为一个DLL。也支持风送复杂类型,比如struct、array等。进阶玩法大家就自己去研究吧,本文就写到这里。
参考资料:
https://github.com/3F/DllExport/
https://github.com/dotnet/ILMerge/
https://en.wikipedia.org/wiki/Assembly_(CLI)
https://en.wikipedia.org/wiki/Dynamic-link_library