工具:Microsoft Visual Studio 2017,已安装Windows x86/x64 native开发的工作负载
看到这个帖子说缺个教程https://discourse.juliacn.com/t/topic/1586我就打算写一个简单暴力的C++ Julia互相调用的教程
【注意】以下带有【注意】的内容包含重要caveat,请务必阅读
首先,启动VS
点击创建新项目
,选择Visual C++>Windows 桌面>Windows 桌面向导
然后添加一个C++源文件
main.cpp
【注意】32位程序只能加载32位DLL,64位程序只能加载64位DLL。我的Julia是64位,所以
然后,写代码
#include <Windows.h>
#include <cstdint>
int64_t GiveMeFive() {
return 5;
}
using byte = unsigned char;
void WriteToBuffer(byte* buffer, size_t length) {
for (size_t offset = 0; offset < length; ++offset) {
buffer[offset] = (byte)offset;
}
}
void WINAPI StdCallConventionAndDifferentName() {
static const char text[] = "This is a stdcall function\n";
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
//Assume the default code page is 936(GBK)
WriteFile(hStdOut, text, sizeof(text), nullptr, nullptr);
}
using Callback = int64_t(*)(int64_t x);
int64_t CallMeBack(Callback callback, int64_t x) {
return callback(x);
}
【注意】MSVC的默认调用约定是__cdecl
。32位下调用约定必须吻合,否则可能破坏栈平衡导致程序崩溃。WINAPI和CALLBACK都表示__stdcall
,这也是Win32 API函数的调用约定。64位下的调用约定被统一了,所以没有这个问题。Julia的ccall
默认调用约定即为平台默认的调用约定。
然后,增加模块定义文件
然后,写定义文件
LIBRARY
EXPORTS
GiveMeFive
WriteToBuffer
StdCallConvention=StdCallConventionAndDifferentName
CallMeBack
然后,指定模块定义文件
然后,编译
然后,检查一下编译好的DLL
方便起见,先给你的PATH
环境变量加入C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build
,然后你就可以使用vcvarsall
一键配置MSVC工具链
然后打开cmd
,进入DLL所在的目录,看一下导出表
C:\Users\Azure\Documents\Visual Studio 2017\Projects\CxxJuliaInterop\x64\Release>vcvarsall amd64
**********************************************************************
** Visual Studio 2017 Developer Command Prompt v15.9.7
** Copyright (c) 2017 Microsoft Corporation
**********************************************************************
[vcvarsall.bat] Environment initialized for: 'x64'
C:\Users\Azure\Documents\Visual Studio 2017\Projects\CxxJuliaInterop\x64\Release>dumpbin /exports CXXJULIAINTEROP.dll
Microsoft (R) COFF/PE Dumper Version 14.16.27027.1
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file CXXJULIAINTEROP.dll
File Type: DLL
Section contains the following exports for CxxJuliaInterop.dll
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
4 number of functions
4 number of names
ordinal hint RVA name
1 0 00001070 CallMeBack = ?CallMeBack@@YA_JP6A_J_J@Z0@Z (__int64 __cdecl CallMeBack(__int64 (__cdecl*)(__int64),__int64))
2 1 00001000 GiveMeFive = ?GiveMeFive@@YA_JXZ (__int64 __cdecl GiveMeFive(void))
3 2 00001030 StdCallConvention = ?StdCallConventionAndDifferentName@@YAXXZ (void __cdecl StdCallConventionAndDifferentName(void))
4 3 00001010 WriteToBuffer = ?WriteToBuffer@@YAXPEAE_K@Z (void __cdecl WriteToBuffer(unsigned char *,unsigned __int64))
Summary
1000 .data
1000 .pdata
1000 .rdata
1000 .reloc
1000 .rsrc
1000 .text
【注意】如果不使用模块定义文件强行指定导出的函数名的话,编译器会把名字装饰成奇怪的东西,所以在Julia中你就无法用原来的函数名字调用了。有的教程会告诉你要extern "C"
,意思是按照C的规范进行链接,而MSVC在32位下的C函数也会进行装饰,64位下却不会。所以此处推荐使用DEF文件解决问题。
然后,enjoy it
julia> const dll="CxxJuliaInterop.dll";
julia> ccall((:GiveMeFive,dll),Int64,())
5
julia> buf=Array{UInt8}(undef,10);
julia> ccall((:WriteToBuffer,dll),Cvoid,(Ptr{UInt8},Csize_t),buf,length(buf))
julia> buf
10-element Array{UInt8,1}:
0x00
0x01
0x02
0x03
0x04
0x05
0x06
0x07
0x08
0x09
julia> ccall((:StdCallConvention,dll),stdcall,Cvoid,())
This is a stdcall function
julia> f(x)=2x
f (generic function with 1 method)
julia> pf=@cfunction(f,Int64,(Int64,))
Ptr{Nothing} @0x000000001f758350
julia> ccall((:CallMeBack,dll),Int64,(Ptr{Cvoid},Int64),pf,42)
84
调用Win32API也很简单:
比如我们想调用GetCursorPos
函数获取鼠标指针坐标,首先翻阅MSDN得到函数原型
BOOL GetCursorPos(
LPPOINT lpPoint
);
并且得知它在User32.dll
中导出
LPPOINT
则是一个POINT
的指针
typedef struct tagPOINT {
LONG x;
LONG y;
} POINT, *PPOINT;
那么就可以这样:
julia> BOOL=Cint
Int32
julia> LONG=Clong
Int32
julia> mutable struct POINT
x::LONG
y::LONG
end
julia> pt=POINT(0,0)
POINT(0, 0)
julia> ccall((:GetCursorPos,"User32.dll"),stdcall,BOOL,(Ptr{Cvoid},),Ref(pt))
1
julia> pt
POINT(1485, 611)
【注意】截至Julia 1.1,无法使用@cfunction
提供平台默认调用约定以外的函数指针,因此32位下无法调用需要回调函数的API,如EnumWindows
。
如果你的DLL需要接收和DLL生命周期相关的回调,可提供一个DLL入口点,否则系统会提供默认的no-op实现。默认入口点名为DllMain
。
给刚才的例子增加以下代码:
#include <cstdio>
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
printf("Module handle: %p\n", hinstDLL);
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
printf("Process attach\n");
break;
case DLL_PROCESS_DETACH:
printf("Process detach\n");
break;
case DLL_THREAD_ATTACH:
printf("Thread attach\n");
break;
case DLL_THREAD_DETACH:
printf("Thread detach\n");
break;
}
return TRUE;
}
hinstDLL
表示DLL的模块句柄,fdwReason
表示调用DllMain
的原因。
当DLL_PROCESS_ATTACH
时返回FALSE
会让LoadLibrary
失败或程序启动失败。
【注意】在DllMain
中很多事情是不可以做的。例如,尝试在DLL加载时调用LoadLibraryA/W
加载其他DLL可能导致死锁。如果进程/线程被Terminate掉,对应的DllMain
回调不会发生。
重新编译DLL,并且使用Julia的Libdl
来控制DLL的加载和卸载:
julia> using Libdl
julia> hModule=Libdl.dlopen("CxxJuliaInterop.dll")
Module handle: 00007FFAB6FA0000
Process attach
Ptr{Nothing} @0x00007ffab6fa0000
julia> GiveMeFive=Libdl.dlsym(hModule,:GiveMeFive)
Ptr{Nothing} @0x00007ffab6fa1100
julia> ccall(GiveMeFive,Int64,())
5
julia> Libdl.dlclose(hModule)
Module handle: 00007FFAB6FA0000
Process detach
true
再次运行dumpbin
查看导出表:
ordinal hint RVA name
1 0 00001170 CallMeBack = ?CallMeBack@@YA_JP6A_J_J@Z0@Z (__int64 __cdecl CallMeBack(__int64 (__cdecl*)(__int64),__int64))
2 1 00001100 GiveMeFive = ?GiveMeFive@@YA_JXZ (__int64 __cdecl GiveMeFive(void))
3 2 00001130 StdCallConvention = ?StdCallConventionAndDifferentName@@YAXXZ (void __cdecl StdCallConventionAndDifferentName(void))
4 3 00001110 WriteToBuffer = ?WriteToBuffer@@YAXPEAE_K@Z (void __cdecl WriteToBuffer(unsigned char *,unsigned __int64))
可以发现,DLL的模块句柄值其实就是DLL被加载到目标进程地址空间内的基址,而DLL中的函数位置则是基址+RVA
。使用GetModuleHandle
函数可以获取当前进程加载的模块句柄:
julia> hModule=Libdl.dlopen("CxxJuliaInterop.dll")
Module handle: 00007FFAB6FA0000
Process attach
Ptr{Nothing} @0x00007ffab6fa0000
julia> ccall(:GetModuleHandleW,Ptr{Cvoid},(Cwstring,),"CxxJuliaInterop.dll")
Ptr{Nothing} @0x00007ffab6fa0000
GetModuleHandleA/W
是Kernel32.dll
中的函数,因此不需要指定DLL。