在Windows下编写C/C++ DLL与Julia调用的正确姿势

工具:Microsoft Visual Studio 2017,已安装Windows x86/x64 native开发的工作负载

看到这个帖子说缺个教程https://discourse.juliacn.com/t/topic/1586我就打算写一个简单暴力的C++ Julia互相调用的教程

【注意】以下带有【注意】的内容包含重要caveat,请务必阅读

首先,启动VS
点击创建新项目,选择Visual C++>Windows 桌面>Windows 桌面向导


p2
然后添加一个C++源文件main.cpp

【注意】32位程序只能加载32位DLL,64位程序只能加载64位DLL。我的Julia是64位,所以
p4

然后,写代码

#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/WKernel32.dll中的函数,因此不需要指定DLL。

8 个赞

很遗憾一直没学会用 MSVS, 只会暴力地用:

CMake -G "Visual Studio ..." +

#ifdef _WIN32
#    ifdef LIBRARY_EXPORTS
#        define LIBRARY_API __declspec(dllexport)
#    else
#        define LIBRARY_API __declspec(dllimport)
#    endif
#elif
#    define LIBRARY_API
#endif
#ifdef __cplusplus
extern "C" {
#endif

#ifdef __cplusplus
} // extern "C"
#endif

正不知道怎么调用呢,多谢分享

图形界面配置起来比较直观
MSVC命令行手动编译的话也可以

cl /LD XXX.cpp /link /DEF:library.def

/link后面都是给链接器的参数

其实微软是比较推荐用__declspec的,然而它不解决根本问题所以我不太爱用…

1 个赞

你好,编译之后的这一步是要怎么做呢?没有看懂你说的这个什么一键配置这个,请问能说一下具体怎么做吗?