Natives/Parameters

A native is a C or C++ function that can be used in Small scripts. They are one of the two ways to glue Small to your module. Take this sample native:

1
2
3
4
5
// module 中定义、实现和注册native函数
static cell AMX_NATIVE_CALL my_native(AMX *amx, cell *params)
{
return 1;
}

This native simply returns 1 - it does nothing else. Let’s analyze it.

  • static - You should know the static keyword.
  • cell - This is the base type for Small plugins. It is either a 4byte or 8byte unsigned integer depending on the architecture.
  • AMX_NATIVE_CALL - a macro for the calling convention (__cdecl).
  • AMX *amx - a pointer to the AMX structure for a plugin. This structure contains the virtual machine bytecode and state information - you rarely need to use this directly.
  • cell *params - an array of parameters that the plugin passed to your native. params[0] is the number of bytes following in the array, so *params/sizeof(cell) will be equal to the number of parameters. Then params[1] is the first parameter and so on.
  • return 1; - AMX functions should always return a value.
    So, how do extract parameters? Let’s say we wanted to make a simple math function that found the product of two numbers passed.
  1. cell是Small脚本的基本类型,从plugin传到Module时,需要使用MF_GetAmxAddr、MF_GetAmxString、MF_GetAmxStringLen等函数来进行转换,才能得到int、char这些类型。
  2. *params == params[0],表示params所有参数的字节数总和,*params/sizeof(cell)可以计算得出有多少个参数传了进来。
  3. 在module中声明native函数,需要按照上边约定的格式来声明。
  4. AMX *amx这个参数,是当我们在Small脚本(*.amxx后缀的插件)中,调用module的native函数时自动传入的,表示当前调用native函数的Small脚本本身的对象实例,即*amx指向当前的Small脚本。
  5. native是被调用的函数,是被动的,在module定义和实现之后,在Small脚本中声明后调用。

Simple enough, right? Well, it gets more complicated. Small plugins follow a certain calling convention.

Integers and floats are passed by value. Arrays (including strings) are passed by reference. And lastly, variable arguments are always passed by reference. For example:

1
2
// Small脚本中声明方式
native sample_native(a, b, ...);

Everything after b will be by reference.

整型、浮点型,都是通过值的方式传参的;数组、字符串、可变长参数,则是通过引用的方式传参的。
可变长参数使用...表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// native prototype: amx_product(...); // Small脚本中声明方式
// Small中使用了可变长参数,对应的在module中,*params都是引用传参
static cell AMX_NATIVE_CALL amx_product(AMX *amx, cell *params)
{
int i = 0;
int sum = 1;
cell *addr = NULL;
int numParams = *params/sizeof(cell);
for (i=1; i<=numParams; i++)
{
// 这里因为是可变长参数 ...
// 所以传入的参数是个引用,不是值传参,不能直接读取值
// 而是先获取它的指针地址addr,在通过(*addr)来获取cell类型的值
addr = MF_GetAmxAddr(amx, params[i]);
sum *= (int)(*addr);
}
return sum;
}

MF_GetAmxAddr is the first module function we will deal with. It takes two parameters, an AMX virtual machine and a by-reference offset in the machine. It will return the actual address where the data is stored. So here, we retrieve the cell address at which the parameter is stored and add it to our product.

What about floats? Let’s say you want to make a function that multiplies two floats together.

1
2
3
4
5
6
7
8
9
10
11
12
//amx_fProduct(Float:a1, Float:a2)
static cell AMX_NATIVE_CALL amx_fProduct(AMX *amx, cell *params)
{
REAL product;

// 这里因为是Float类型,而且是固定参数的值传参
// 而对于值传参,可以直接读取值
// 所以,这里不需要MF_GetAmxAddr,而是直接获取cell类型的值
product = amx_ctof(params[1]) * amx_ctof(params[2]);

return amx_ftoc(product);
}

Here we have the next two module functions. The AMX virtual machine has only one data type - the cell. So we have two natives to convert a cell to a float and vice versa. Because cells differ from platform to platform, we use REAL which is a float or a double depending on the cellsize. amx_ctof convers a cell to a REAL and amx_ftoc converts a REAL to a cell. Note we have to return the result as a cell, not a REAL.

整型和浮点型,在Small通过native函数传入的参数中,都是cell类型的,对于整型的话,可以直接把cell强制转化为整型。而对于浮点型,在32位和64位的系统中是不同的,它可能是float或者double类型,为了屏蔽差异,我们可以用REAL类表示浮点型,我们可以使用amx_ctof把cell转成REAL类型,相反地,我们可以使用amx_ftoc把REAL类型转成cell类型。

Here is the variadic version using multiple parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//native prototype: Float:amx_fProduct({Float}:...);
static cell AMX_NATIVE_CALL amx_product(AMX *amx, cell *params)
{
int i = 0;
REAL sum = 1;
cell *addr = NULL;
int numParams = *params/sizeof(cell);
for (i=1; i<=numParams; i++)
{
addr = MF_GetAmxAddr(amx, params[i]);
sum *= amx_ctof(*addr);
}

return amx_ftoc(sum);
}

Now, what about strings? There are two issues with strings. Strings are basically arrays, which means each character is a 32bit cell, not a char. To help convert there is a function called MF_GetAmxString:

1
char *MF_GetAmxString(AMX *amx, cell addr, int bufferId, int *length);

Buffer IDs are from 0 to 3. They copy the string to a static, non-reentrant buffer. This means you can only use this function with 4 string parameters, and that it is not thread safe!

  • 对于string来说,Small脚本中是以cell类型数组表示的,那么当它传到module中时,也是cell类型的,我们需要通过MF_GetAmxString来对它进行转换,从而得到我们的char类型。
  • 关于MF_GetAmxString函数的bufferId,指的是Small虚拟机中ID编号为0到3的固定内存区域,专门用于MF_GetAmxString函数的,每次调用此函数时,都会把转换得到的char字符串保存到指定0到3编号中的其中一块区域,这个区域是所有脚本共享的,也就是说,其他脚本调用此函数,也会把char内容往这里边放,会覆盖掉我们当前的内容。所以我们在调用完此函数后,就应该第一时间使用strcopy或者strdup等函数,把内容拷贝出来,放置到module的内存中,把指针重新指向module中的内存区域。这样,就不用担心bufferId的内容被覆盖了。

Next, you can “set” strings using MF_SetAmxString. Let’s wrap this section up with a super-example. This function will take in a vector (Float:vec[3]), a string, then multiply the vector by the integer value in the string, then set the result as a string and return a float:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Float:amx_weird(Float:vec[3], const str[], result[], maxlen)
static cell AMX_NATIVE_CALL amx_weird(AMX *amx, cell *params)
{
int len = 0;
cell *vec = MF_GetAmxAddr(amx, params[1]);
char *str = MF_GetAmxString(amx, params[2], 0, &len);
REAL prod = amx_ctof(vec[0]) * amx_ctof(vec[1]) * amx_ctof(vec[2]);
prod *= atoi(str);
char buf[20];
itoa((int)prod, buf, 10);
//amx, dest addr, source, max length
MF_SetAmxString(amx, params[3], buf, params[4]);
return amx_ftoc(prod);
}

认真看完和理解上面的这段代码,你应该会明白如何在Module中与Small(Plugin)传递参数了。只要记住三点:

  1. Small仅支持cell类型,你得想方设法地把cell类型与整型、浮点型、或字符串类型之间进行转换
  2. 只要整型和浮点型使用值传参,其他类型如数组、字符串或可变长变量等,都是引用传参
  3. 浮点型转换成REAL类型,因为Small用cell来表示浮点型时,可能是float,也可能是double

So how do you glue your natives to small scripts? This is a two step process.
First, go to moduleconfig.h and uncomment this line:

1
2
// moduleconfig.h,取消下边的注释,即可开启OnAmxxAttach回调函数
// #define FN_AMXX_ATTACH OnAmxxAttach

Then add this somewhere into your module’s main file (let’s call it mymodule.cpp):

1
2
3
4
5
6
7
// module main.cpp
AMX_NATIVE_INFO my_Natives[] =
{
{"amx_fproduct", amx_fProduct},
{"amx_weird", amx_weird},
{NULL, NULL},
};

This creates a list describing what natives you want to add to Small plugins. The list is formatted as Name, Function, Name, Function, etc. Terminate the list with NULLs. Now create this function:

1
2
3
4
5
6
// module main.cpp OnAmxxAttach回调函数实现
// 官方建议在此处注册我们的native函数
void OnAmxxAttach()
{
MF_AddNatives(my_Natives);
}

Now your functions are available to plugins! The last step is to actually make an include file. Example mymodule.inc:

1
2
3
4
5
6
7
8
9
10
11
// Small mymodule.inc 在Small脚本中通过native关键字声明后,即可调用module中的native函数
#if defined _mymodule_included
#endinput
#endif
#define _mymodule_included

//Multiply all the float parameters passed and return the result.
native Float:amx_fproduct({Float}:...);

//This function is just weird!
native Float:amx_weird(Float:vec[3], const numStr[], retstr[], maxLen);

如何编写和调用native函数?

  1. 在module中,声明native结构,实现native代码,注册native函数。
  2. 在small中,声明native结构(与module是不一样的),然后就可以直接调用了。
    最后,给出一个例子说明native函数的调用和传参过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// module 声明native
static cell AMX_NATIVE_CALL getIpInfoByStr(AMX *amx, cell *params);

// Small 声明native
native getIpForPlugin(const ipStr[], retSIP[], retEIP[], retA[], retB[]);

/*
两者的关系就是:
1. getIpForPlugin是MF_AddNatives中传递的nativeInfo定义的Small函数名称,指向的是module中的getIpInfoByStr函数
2. 在Small中调用getIpForPlugin函数,其本质上是调用module中的getIpInfoByStr函数
3. getIpInfoByStr的AMX *amx参数,是Small的实例
4. params[0],表示参数字节数的总和,*params/sizeof(cell)或者params[0]/sizeof(cell)可得到参数总个数
5. params[1] == ipStr
6. params[2] == retSIP
7. params[3] == retEIP
8. params[4] == retA
9. params[5] == retB
*/

You’re done!

Forwards/Callbacks

The second way to glue your module to plugins is to make callbacks, or forwards.

在module中,除了native函数被动的被Small调用外,我们还有forward函数,能够通过回调的方式,主动地执行Small中的函数。

当我们的module模块,hook到系统某一事件,或者定时器触发了某一项task时,我们需要主动地去执行Small中的某函数时,我们就需要用到forward函数了。

除了Native,Forward也是module与Small联动的另外一种方式。

For example, let’s say you want your module to have two new forwards for plugins: one so they can hook every time something is touched, another so they can hook every time an entity is created by name - but only if they register the call beforehand. The first example is a global forward. In an include, it would look like this:

1
2
// Small 声明和实现forward回调函数。
forward mymodule_Touch(entToucher, entTouched);

To make a global forward is pretty simple. These examples will require Metamod activation in your module. Find these lines in moduleconfig.h and uncomment them:

1
2
3
4
// #define USE_METAMOD
// #define FN_AMXX_PLUGINSLOADED OnPluginsLoaded
// #define FN_CreateNamedEntity CreateNamedEntity
// #define FN_DispatchTouch DispatchTouch

In your module header file (mymodule.h) add something like:

1
2
3
4
5
6
7
8
9
10
11
12
#include "CString.h"	//copy this file from AMX Mod X core
#include "CVector.h" //copy this file from AMX Mod X core
#include "amxxmodule.h" //this is the SDK file

struct CreateForward //We will use this to store some info
{
int forwardID;
String name;
};

extern int TouchForward;
extern CVector<CreateForward> CreateForwards;

In mymodule.cpp, you can now do:

官方建议是在OnPluginsLoaded()中注册forward函数

MF_RegisterForward、MF_RegisterSPForward或者MF_RegisterSPForwardByName的最后一个参数,必须是FP_DONE

其他中间的参数,即声明forward函数的参数格式,可以为FP_CELL、FP_FLOAT、FP_STRING、FP_STRINGEX和FP_ARRAY,例如:

1
TouchForward = MF_RegisterForward("mymodule_Touch", ET_IGNORE, FP_CELL, FP_CELL, FP_DONE);

那么Small对应的forward就应该有两个cell参数,即:

1
forward mymodule_Touch(entToucher, entTouched);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int TouchForward = -1;	//-1 is an invalid forward so initiate this to that
CVector<CreateForward> CreateForwards;

void OnPluginsLoaded()
{
//mymodule_Touch - the forward name
//ET_IGNORE - means PLUGIN_HANDLED will be ignored (other option is ET_STOP)
//FP_CELL - first parameter is a normal cell
//FP_CELL - second parameter is a normal cell
//FP_DONE - that's the end of the parameter descriptions
TouchForward = MF_RegisterForward("mymodule_Touch", ET_IGNORE, FP_CELL, FP_CELL, FP_DONE);
}

void DispatchTouch(edict_t *pfnToucher, edict_t *pfnTouched)
{
if (TouchForward > -1)
{
MF_ExecuteForward(TouchForward, ENTINDEX(pfnToucher), ENTINDEX(pfnTouched));
}

//ignore metamod return value
RETURN_META(MRES_IGNORED);
}

这里MF_RegisterForward注册的是全局的forward,即每个Small插件都可以声明和实现这个名为mymodule_Touch的forward。

值得注意的是,由于是全局的forward,就有可能存在多个Small都实现了mymodule_Touch这个forward函数。此时,Module就会按照Small脚本加载的顺序,把所有Small的forward组织起来,形成一个forward函数的调用链条。那么,MF_RegisterForward的第2个参数,就会发挥它的作用。

MF_RegisterForward的第2个参数,可以取值ET_IGNORE或ET_STOP,两者的区别就是,如果在Small的forward中,当遇到return PLUGIN_HANDLED时,是否仍然继续往下执行链条中其他Small的forward函数。ET_STOP表示不再往下执行,ET_IGNORE表示忽略PLUGIN_HANDLED继续往下执行其他的forward函数。而非全局的MF_RegisterSPForward和MF_RegisterSPForwardByName就不需要,也没有ET_IGNORE和ET_STOP参数。

接下来介绍的是如何注册非全局(独属于某个Small)的forward函数,区别与全局的forward注册,非全局使用的函数是MF_RegisterSPForward或者MF_RegisterSPForwardByName,其中SP的意思就是single plugin英文的缩写:

在官方的示例中,使用的是MF_RegisterSPForwardByName函数,如果你要使用MF_RegisterSPForward的话也是可以的,但是可能还需要MF_AmxFindPublic()来搭配使用,才能找到参数int func的函数指针。

Now your module will call the forward in every plugin, every time two entities touch. The next part is trickier, we want a native that will register forwards, for example:

1
2
3
4
5
6
7
public plugin_info()
register_create("createWeaponBox", "weaponbox")

public createWeaponBox(classname[], entId)
{
//A weaponbox has been created!
}

This is a non-global, or Single Plugin forward. Here is an example of how to make one:

这里官方的代码是有错误的,MF_RegisterSPForwardByName函数缺少了amx参数
因为本来这个函数就是针对某个Small来注册forward的,所以不能缺少amx(Small的实例)
MF_RegisterSPForwardByName(amx, func, FP_STRING, FP_CELL, FP_DONE);

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
//register_native(func[], classname[])
static cell AMX_NATIVE_CALL register_create(AMX *amx, cell *params)
{
int len = 0;
//get function name
char *func = MF_GetAmxString(amx, params[1], 0, &len);
//get desired classname
char *classname = MF_GetAmxString(amx, params[2], 1, &len);

//new forward storage
CreateForward sForward;
//set the classname
sForward.name.assign(classname);
//note this time there is no ET_ value. Also, FP_STRING is a special format option.
//only use FP_STRING for valid char * zero terminated string pointers.
//If you need to push an array, you must use FP_ARRAY and MF_PrepareCellArrray
sForward.forwardID = MF_RegisterSPForwardByName(func, FP_STRING, FP_CELL, FP_DONE);

// 这里官方的代码是有错误的,MF_RegisterSPForwardByName函数缺少了amx参数
// 因为本来这个函数就是针对某个Small来注册forward的,所以不能缺少amx(Small的实例)
// MF_RegisterSPForwardByName(amx, func, FP_STRING, FP_CELL, FP_DONE);

if (sForward.forwardID == -1)
{
MF_LogError(amx, AMX_ERR_NOTFOUND, "Function \"%s\" was not found!", func);
return 0;
}

//add this information to the main list
CreateForwards.push_back(sForward);
return 1;
}

//This is the MetaMod hook for catching when an entity is made.
edict_t *CreateNamedEntity(int iszClassname)
{
//go through our list of forwards. do any of the classes match?
//STRING converts an HL engine isz string to an sz string
const char *classname = STRING(iszClassname);
int i = 0;
edict_t *e = CREATENAMEDENTITY(iszClassname);
for (i=0; i<CreateForwards.size(); i++)
{
if (CreateForwards[i].forwardID != -1)
{
if (CreateForwards[i].name.compare(classname) == 0)
{
MF_ExecuteForward(CreateForwards[i].forwardID, classname, ENTINDEX(e));
}
}
}
RETURN_META_VALUE(MRES_SUPERCEDE, e);
}

There! We’ve now made global and local forwards.

非全局的forward实现的方法,其实就是利用Small调用native函数时会把AMX *amx参数传入到module中特性,在module中编写一个专门用来注册forward的函数,这样就可以结合AMX *amx参数,使用带SP的MF_RegisterSPForwardByName函数,把forward单独地注册到这个Small脚本上了。

Compiling

Lastly, you must compile your module. For Windows, this is fairly easy. Microsoft makes some of the best development environments known - so we have some directions for Microsoft Visual C++ 6.0 and 7.0 (and gcc for Linux users)

Microsoft Visual C++ 6.0

  1. Create a new project from the File menu.
  2. Select Win32 Dynamic-Link Library.
  3. On the next page, select “An empty DLL project”.
  4. Go to Projet, Add to Project, Files.
  5. Add amxxmodule.cpp, amxxmodule.h, moduleconfig.h, and the other files you have for your module.
  6. Go to Project, Settings. Change these settings for both Win32 Debug and Win32 Release options.
  7. Under C++ / Code Generation, it is recommended you do not make Multi-Threaded builds.
  8. Under Link, you should change the module name to conform to AMX Mod X standards: _amxx.dll.
  9. Once you’re done, go to Tools, Options, Directories.
  10. Make sure you have Metamod and HLSDK source code installed.
  11. Add the following directories above the already included ones:
  • in hlsdk/SourceCode or hlsdk/multiplayer/ folder: pm_shared, engine, common, dlls.
  • Metamod’s source folder.
  • AMX Mod X’s source folder if you include AMXXSTL files with #include <>.
  1. You’re ready to build your module from the Build menu. Use Set Active Configuration to switch between Debug and Release.

Microsoft Visual C++ 7.0

  1. Go to File, New, Project.
  2. Select Visual C++ Projects, Win32 Application.
  3. Select Application Settings, DLL, Empty Project.
  4. Go to Project, Properties. For both Debug and Release, change these:
  • Linker->General should conform to AMX Mod X’s naming scheme, _amxx.dll
  • C/C++->Code Generation->Runtime Library should never be Multi-Threaded (unless special circumstances need it).
  1. Lastly, in the Tools, Options, Projects, VC++ Directories, Include Files menu, make sure to include the same directories as shown above for VC++ 6.0

Linux (GNU C Compiler 2.95 and 3.30)

  1. AMX Mod X is distributed with perl Makefiles to assist you in building your modules. They are fairly easy to edit - you can just change the array containing the source files. For 32bit machines, you can use a GCC line like this:
1
2
3
gcc -march=i386 -O3 -fomit-frame-pointer -s -Wall -fno-rtti    \
-fno-exceptions -fPIC <list of source files> -ldl -shared \
-lm -o<modulename>_i386.so

For 64bit, it becomes:

1
2
3
gcc -m64 -DSMALL_CELL_SIZE=64 -DHAVE_I64 -O3 -fomit-frame-pointer \
-s -Wall -fno-rtti -fno-exceptions -fPIC <list of source files> \
-ldl -shared -lm -o<modulename>_amd64.so
  1. If you use the Standard C++ Library, you may need to include -lstdc++, just be aware that not everyone may have your version of stdc++.

  2. To test if your module will load on linux, you can use the “dlsym” or “dlsym64” program that comes with AMX Mod X for Linux. It will either return an error message or success notification:

1
2
cd amxmodx/scripting
./dlsym ~/mymodule/mymodule_i386.so

Debugging(crashes)

I’m not going to go into how to debug too in depth, as it’s a complicated subject best learned by practice. But I will go over how to start and see a debugger and a call stack trace (showing what happened prior to the crash). Since MSVC 7.0’s debugger is much easier to use than 6’s, I will not cover 6’s.

Microsoft Visual C++ 7.0

  1. Compile your module in debug mode. You may also want to compile and upload debug versionso of AMX Mod X and Metamod, incase the bug ends up being there.
  2. Start HLDS with -dev 1. This gives metamod a 10 second pause so you can attach to the HLDS process. You could also just use HLDS GUI and attach before you click “Start”.
  3. In MSVC, go to Tools, Debug Process. Attach to the HLDS process.
  4. Cause your module to crash or hit a breakpoint.
  5. In MSVC, go to Debug, Windows, Call Stack.

Linux (GNU Debugger)

  1. compile your module with only the following optimization flags:
1
2
3
-ggdb3
(or for AMD64:)
-ggdb3 -DSMALL_CELL_SIZE=64 -DHAVE_I64
  1. cd into the directory where hlds_run is located. Type the following line into your shell/command line:
1
export LD_LIBRARY_PATH=".:$LD_LIBRARY_PATH"
  1. Make sure you have GDB (GNU Debugger installed). Enter the debugger:
1
2
3
gdb hlds_i686
(or on AMD64)
gdb hlds_amd64
  1. Load HLDS:

run +maxplayers 16 +map de_dust -game cstrike -debug

  1. Cause your module to crash, or hit a previously specified breakpoint.

  2. Now you can use the GNU Debugger to view a call stack with any of these commands:

1
2
3
bt
where
backtrace
  1. Or if you want a more advanced diagnostic:
1
info all-registers

For both debuggers, there are many more options for powerful debugging. It is best to consult the manuals to see how to use them.

总结

  1. 在Module和Small(Plugin)之间,两者是通过Native和Forward进行联动的。
  2. Native是在Module中实现,由Small主动调用
  3. Forward是在Small中实现,由Module回调调用(可以用做Hook和task的场景)
  4. Forward分为全局的forward和非全局的forward,使用的注册函数不同
  5. Native是在OnAmxxAttach()注册
  6. Forward是在OnPluginsLoaded()全局注册,也可以是在Native中由Small调用来进行非全局注册

参考文献

AMX Mod X Documentation