虽然 NSIS 官方提供了很多插件,也有一些第三方的插件可以下载使用,但在实际开发中难免会遇到已有插件没有提供的功能或者插件不好用的情况,这时我们可以选择自己开发 NSIS 插件。

一、插件开发规范

NSIS 插件对开发语言没有限制(本文介绍基于 C++开发),只要按照 NSIS 标准所要求的生成一个 dll 文件,该 dll 文件提供纯C规范的固定格式的导出函数即可。

总结起来,开发 NSIS 插件的步骤为:

  1. 使用 Visual Studio 新建 dll 工程(运行库选择MTMTD),建议使用的 MT 运行库,不需要额外携带运行时文件。

  2. 定义插件功能函数,假设函数名为 add,则函数声明格式如下:

    1
    2
    3
    4
    5
    6
    extern "C" __declspec(dllexport) void __cdecl add (
    HWND hwndParent,
    int string_size,
    char *variables,
    stack_t **stacktop,
    extra_parameters *extra);
  3. 编译生成 dll 文件。

二、参数和返回值

开发 NSIS 插件的难点在于:获取调用者传入的参数,以及将执行结果返回给调用者。
前面的 NSIS教程(7)-插件使用 说到了,插件函数的调用者传递参数方式有 2 种:

  • 一种是通过$0~$9 $R0~$R9的形式,这种形式的参数通过char *variables参数来获取。
  • 一种是通过堆栈的形式,这种形式的参数通过stack_t **stacktop参数来获取。

三、PluginCommon

为了方便插件开发中获取用户传入参数、以及执行结果的返回,我对插件开发的常用操作进行了封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// PluginCommon.h
#ifndef _PLUGIN_COMMON_H_
#define _PLUGIN_COMMON_H_

#include <stdio.h>

#define PLUGIN_BUF_LEN 1024

#define NSISAPI extern "C" __declspec(dllexport) void __cdecl


#define EXDLL_INIT() { \
g_stringsize=string_size; \
g_stacktop=stacktop; \
g_variables=variables; }

typedef struct _stack_t {
struct _stack_t *next;
char text[1];
} stack_t;


static unsigned int g_stringsize;
static stack_t **g_stacktop;
static char *g_variables;

enum
{
INST_0, // $0
INST_1, // $1
INST_2, // $2
INST_3, // $3
INST_4, // $4
INST_5, // $5
INST_6, // $6
INST_7, // $7
INST_8, // $8
INST_9, // $9
INST_R0, // $R0
INST_R1, // $R1
INST_R2, // $R2
INST_R3, // $R3
INST_R4, // $R4
INST_R5, // $R5
INST_R6, // $R6
INST_R7, // $R7
INST_R8, // $R8
INST_R9, // $R9
INST_CMDLINE, // $CMDLINE
INST_INSTDIR, // $INSTDIR
INST_OUTDIR, // $OUTDIR
INST_EXEDIR, // $EXEDIR
INST_LANG, // $LANGUAGE
__INST_LAST
};


static int __stdcall popstring(char *str)
{
stack_t *th;
if (!g_stacktop || !*g_stacktop) return 1;
th=(*g_stacktop);
lstrcpyA(str,th->text);
*g_stacktop = th->next;
GlobalFree((HGLOBAL)th);
return 0;
}

static void __stdcall pushstring(char *str)
{
stack_t *th;
if (!g_stacktop) return;
th=(stack_t*)GlobalAlloc(GPTR,sizeof(stack_t)+g_stringsize);
lstrcpynA(th->text,str,g_stringsize);
th->next=*g_stacktop;
*g_stacktop=th;
}

static int __stdcall popint()
{
char buf[512] = {0};
popstring(buf);
return atoi(buf);
}

static void __stdcall pushint(long value)
{
char buf[512] = {0};
sprintf_s(buf, "%ld", value);
pushstring(buf);
}

static char * __stdcall getuservariable(int varnum)
{
if (varnum < 0 || varnum >= __INST_LAST) return NULL;
return g_variables+varnum*g_stringsize;
}

static void __stdcall setuservariable(int varnum, char *var)
{
if (var != NULL && varnum >= 0 && varnum < __INST_LAST)
lstrcpyA(g_variables + varnum*g_stringsize, var);
}

enum NSPIM
{
NSPIM_UNLOAD, // This is the last message a plugin gets, do final cleanup
NSPIM_GUIUNLOAD, // Called after .onGUIEnd
};

typedef UINT_PTR (*NSISPLUGINCALLBACK)(enum NSPIM);

// extra_parameters data structures containing other interesting stuff
// but the stack, variables and HWND passed on to plug-ins.
typedef struct
{
int autoclose;
int all_user_var;
int exec_error;
int abort;
int exec_reboot; // NSIS_SUPPORT_REBOOT
int reboot_called; // NSIS_SUPPORT_REBOOT
int XXX_cur_insttype; // depreacted
int plugin_api_version; // see NSISPIAPIVER_CURR
// used to be XXX_insttype_changed
int silent; // NSIS_CONFIG_SILENT_SUPPORT
int instdir_error;
int rtl;
int errlvl;
int alter_reg_view;
int status_update;
} exec_flags_t;

#ifndef NSISCALL
#define NSISCALL __stdcall
#endif

typedef struct {
exec_flags_t *exec_flags;
int (NSISCALL *ExecuteCodeSegment)(int, HWND);
void (NSISCALL *validate_filename)(char *);

// returns 0 on success, 1 if already registered and < 0 on errors
int (NSISCALL *RegisterPluginCallback)(HMODULE, NSISPLUGINCALLBACK);
} extra_parameters;

#endif //_PLUGIN_COMMON_H_

在开发插件中经常用到的有:
4 个堆栈操作函数:pushint,pushstring,popint,popstring
2 个寄存器变量($R0~$R9,$0~$9)操作函数:getuservariablesetuservariable

四、示例

基于上面的 PluginCommon.h,我们可以将开发插件的步骤流程大大简化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <windows.h>
#include <commctrl.h>
#include <stdarg.h>
#include <tchar.h>
#include "PluginCommon.h"

HINSTANCE g_hInstance;
HWND g_hwndParent;
extra_parameters *g_pluginParms = NULL;

#define NSMETHOD_INIT(parent) {\
g_pluginParms = extra; \
g_hwndParent = parent; \
EXDLL_INIT(); }


BOOL WINAPI DllMain(HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved)
{
g_hInstance = (HINSTANCE)hInst;

if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
//do what you want at init time.
}

if (ul_reason_for_call == DLL_THREAD_DETACH || ul_reason_for_call == DLL_PROCESS_DETACH) {
//clean up code.
}

return TRUE;
}

//
// 以上内容通用,每个插件都可以将上面的内容直接复制过去。
//


// NSIS插件导出函数,以add函数为例,若要添加其他函数,则只是函数名需要修改,函数逻辑写在NSMETHOD_INIT(hwndParent);之后的花括号内。
//
extern "C" __declspec(dllexport) void __cdecl
add ( HWND hwndParent, int string_size, char *variables, stack_t **stacktop, extra_parameters *extra)
{
NSMETHOD_INIT(hwndParent);
{
// == 添加自己代码
// 假设调用者通过堆栈的形式传递的参数
int i = popint(); // 注意:栈是先进后出
int j = popint();
int k = i + j;
pushint(k); // 通过plugin-common.h中提供的pushint返回整数,也可以使用pushstring返回字符串
// ==
}
}

五、Unicode 支持

大家可能注意到了上面的示例和PluginCommon.h中使用的都是char,而不是wchar_t。那如果要改成wchar_t需要哪些工作了?

  1. 将插件代码中的char改成wchar_t
  2. 在 nsi 脚本中加入Unicode True
  3. 需要将生成的插件 dll 放到 nsis 目录下的Plugins\x86-unicode子目录中。