Extensions

From Bohemia Interactive Community
Jump to navigation Jump to search

This topic is about the dlls addons may supply, that a mission or addon may use with the callExtension script command. This functionality arrived in an arma2 post-1.60 beta, making 1.61 the first stable version to support it. Linux (server) support was not available until Arma3 version 1.18.

So far this functionality had been achieved by JayArma2Lib, but it was brittle, requiring an update for every beta and patch released.

For players

DLLs are natively executed code. It has access to the computer as the user account arma is running as. Anything you can do to the computer (without invoking a UAC prompt, the yellow/blue shield) the dll can do.

Prior to extensions, an addon maker was quite limited in what harm he could cause to the system. About the worst is bad effects to gameplay whenever the addon is loaded - stop loading it and all will again be fine. With extensions, this is no longer quite the case; the bad addon could potentially destroy everything you have on that computer, or start interfering with online banking. (In theory, JayArma2Lib, precursor to extensions, could do the same. We don't have any reason to think it actually did.)

Extensions may require other libraries and runtimes to be installed on the computer it is to run on. For example, if it is compiled with the Microsoft Visual C++ 2008 compiler, the corresponding runtime library must be installed. If it accesses a mysql database, the mysql client library dlls may need to be installed.

Defending against malicious extensions

Establish that the extension writer has reputation on the line. If it's somebody nobody ever heard of, who could just disappear again without a lot of people noticing, then the risk is higher. Even if he were to be exposed, it would happen at no cost to him.

Make sure the sources are provided. Even if you can't read them, someone else can, and there is a reasonable chance someone will find any backdoors. This increases the risk of getting the crimes exposed; see above.

Whoever compiles the sources into a dll can add whatever they want, last minute, in a separate copy from the sources added. You'll be trusting whoever compiles them for you, trustworthy or not. (They can also be tampered with after the dll has been built, but this is harder. Forensics should be able to tell the difference.)


The easy way out is to simply trust the people involved, and take the fall if/when you do get hacked from it. Problem is, you might not know.

Defending against vulnerable extensions

Another concern is when the extension is vulnerable. Security vulnerabilities have plagued a lot of software. An extension may well have buffer overflows and other problems which an attacker can exploit to gain access to your system.

If the sources are available, it's possible to see if the programmer has employed coding practices which decrease the risk of buffer overflows and similar basic bugs. This provides no guarantee, but can inspire confidence. By default, we should assume that such practices are not used: most tutorials, university courses, etc teach the new programmer to do it in the worst possible way. (I'll spare you the rant.)

For server admins

As server admin, your selection of mods will have some role in choosing what addons the users will be installing - and thus what extensions they are using. Many users will blindly follow your instructions, which makes the for users section content even more important for you.

And we haven't even mentioned using extensions on the server. Server extensions are most often used for persistency purposes. For persistent world servers, however, the above caveats apply even more - these dlls can potentially give a player full access to the server or to the databases. Whether backdoored or exploited.

If you run a linux server, you depend on the extension to also be compiled for linux.

For mission makers

A few notes in no particular order

  • Depending on what that feature is, it may be desirable to make it optional - if the extension is not available, the mission would still work, but without that feature.
  • Using an extension adds a dependency on that extension. At the outset this is no different from a dependency on an addon, except this dependency is never automatically checked unless you're using objects from that addon as well.
  • A user may have an addon downloaded, but not have the extension enabled even though it's supposed to come as part of that addon. (This depends largely on the distribution method.)

Scripting

See callExtension. The exact content of the strings will depend entirely on the extension in use.

For addon makers

Extensions are distributed as part of an addon. Thus this section will end up being rather expansive.

A few considerations

  • Can what you're trying to achieve be done in pure sqf?
  • Keeping in mind the things listed in the players section - why should you be trusted?
  • Making an extension is a programming task. And fairly low-level at that.
  • Determine what is to be the responsibility of the sqf/java side and what is the responsibility of the native side.
  • ... and the interface between the two. You only get text strings, after all. (With netId and objectFromNetId and groupFromNetId you can refer to specific objects in strings.)

A few technical considerations

  • Extensions have no access to the game environment except what sqf scripts (arma3/takonh presumably also java code) are using the return results for. (call compile means the extension can emit sqf code.)
  • Extensions are called only when the sqf code calls it. If you need to do work all the time, you need to call it all the time - or spawn a thread to do it in the background, where appropriate.
  • Currently, the extension code doesn't need to be reentrant - sqf evaluation proceeds single threaded and halts until you return. It is unknown how that may change wrt java, though, so you may want to write as-if you can be called by multiple threads.
  • There is a maximum size for how much data an extension can transfer to the game. When the feature was initially released it was 4k, it has since been raised to 8k, 16k, and is now down to 10kB. So far it has only changed between versions of arma, not while the particular arma process is running.
  • When you need to transfer data larger than said limit, you need an interface for getting the rest of the data in subsequent calls. (See example - TODO)

How to make an extension

Begin by setting up your development environment to create a .dll - dllmain must do the work for initializing and cleaning up, RVExtension is the only function called by the game engine.

(TODO: Insert a couple example projects)

DLL Interface

The DLL is expected to contain an entry point in a form of a function named RVExtension or RVExtensionArgs (exported with decorated name _RVExtension@12 / _RVExtensionArgs@20 / _RVExtensionVersion@8 for 32-bit extension and RVExtension / RVExtensionArgs / RVExtensionVersion for 64-bit extension; Visual C/C++ compilers handle that for you, but if you use other tools or languages this might be significant) and following signature:

void __stdcall RVExtension(char *output, int outputSize, const char *function);
int __stdcall RVExtensionArgs(char *output, int outputSize, const char *function, const char **args, int argCnt);

The game currently calls the RVExtension function with outputSize of 10240 (can be increased in future versions if needed, called extensions should always check this value and never produce output larger than this size). If the function is to receive arguments, they can be concatenated to the function name and the dll is responsible to perform any spliting / parsing / decoding as needed.

The engine supplies (via function) and expects (via output) a UTF-8 encoded C string. Conversion to/from wide character set strings (LPCWSTR/LPWSTR) can be performed using MultiByteToWideChar and WideCharToMultiByte functions.

The example dll source example follows. In this example the dll simply copies the input to the output:

// dllmain.cpp : Defines the entry point for the DLL application.
#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files:
#include <windows.h>
#include <string>

BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

extern "C"
{
  __declspec(dllexport) void __stdcall RVExtension(char *output, int outputSize, const char *function);
  __declspec(dllexport) int __stdcall RVExtensionArgs(char *output, int outputSize, const char *function, const char **args, int argCnt); // Experimental
};

void __stdcall RVExtension(char *output, int outputSize, const char *function)
{
  outputSize -= 1;
  strncpy(output,function,outputSize);
}

int __stdcall RVExtensionArgs(char *output, int outputSize, const char *function, const char **args, int argCnt) // Experimental
{
  outputSize -= 1;
  std::string out = function + ":";
  for (int i = 0; i < argCnt; i++)
  {
    out += args[i];
    out += ', ';
  }
  strncpy(output,function,out.c_str());
}

A starter tutorial on how to make ArmA extension in C++ using Visual Studio Express 2012 can be found here (part1) and here (part2).

A starter tutorial on how to make ArmA extension in C# using Visual Studio Express 2013 can be found here (part3)

A threaded extension example in C++ using Visual Studio Express 2013 can be found here (part4)

Another example written in C# using Visual Studio Express 2013 and without having to use C++/CLR can be found here

Another example, written in the D programming language:

import std.c.windows.windows;
import core.sys.windows.dll;

__gshared HINSTANCE g_hInst;

extern (Windows) BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved) {
	final switch (ulReason) { 
		case DLL_PROCESS_ATTACH:
			g_hInst = hInstance;
			dll_process_attach(hInstance, true);
			break;

		case DLL_PROCESS_DETACH:
			dll_process_detach(hInstance, true);
			break;

		case DLL_THREAD_ATTACH:
			dll_thread_attach(true, true);
			break;

		case DLL_THREAD_DETACH:
			dll_thread_detach(true, true);
			break;
	}

	return true;
}

import std.conv;
import std.exception;

export extern (Windows) void RVExtension(char* output, int output_size, const char* cinput) {
	auto dinput = to!string(cinput);
	auto doutput = output[0 .. output_size];
	string result;
	
	// ...
	
	enforce(result.length <= output_size, "Output length too long");
	doutput[0 .. result.length] = result[];
	doutput[result.length] = '\0';
}

Another example, written in Delphi/Pascal:

library dllTest;

uses
  SysUtils;

{Return value is not used.}
procedure RVExtension(toArma: PAnsiChar; outputSize: Integer; fromArma: PAnsiChar); stdcall; export;
begin
  StrCopy(toArma, fromArmA);
end;

exports
  { 32-bit }
  RVExtension name '_RVExtension@12';
  { 64-bit }
  RVExtension name 'RVExtension';
begin
end.

Linux shared library interface

Extension support was added to Linux in Arma 3 v1.18. The shared library should export the following function:

void RVExtension(char *output, int outputSize, const char *function);

Here's an example of a simple shared library that copies the input to the output, just like the Windows DLL example above:

/*
 * example.c
 *
 *   gcc -shared -fPIC -o example.so example.c
 */
#include <string.h>     // strcmp, strncpy

static char version[] = "1.0";

void RVExtension(char *output, int outputSize, const char *function)
{
    if (!strcmp(function, "version"))
    {
        strncpy(output, version, outputSize);
    }
    else
    {
        strncpy(output, function, outputSize);
    }
    output[outputSize-1]='\0';

    return;
}

Extension existence/version checking

Each extension should include a function which allows scripts to test for existence (and version) of the extension. When the extension is not present, calls to callExtension return an empty string. When you provide a function "version" returning e.g. "1.0", anyone can test if the extension is present by calling this function.

void __stdcall RVExtension(char *output, int outputSize, const char *function)
{
  outputSize -= 1;
  if (!strcmp(function,"version"))
  {
    strncpy(output,"1.0",outputSize);
  }
  else
  {
    strncpy(output,function,outputSize);
  }
}

How to make an extension in C#

Writing an extension in C# requires to export the interface entry point. This can be handled by libraries like DllExport or Unmanaged Exports].

The entry points can be in any class, this doesn't matters. This is the minimal example of a working C# extension:

class DllEntry {

        /// <summary>
        /// Gets called when arma starts up and loads all extension.
        /// It's perfect to load in static objects in a seperate thread so that the extension doesn't needs any seperate initalization
        /// </summary>
        /// <param name="output">The string builder object that contains the result of the function</param>
        /// <param name="outputSize">The maximum size of bytes that can be returned</param>
#if WIN64
        [DllExport("RVExtensionVersion", CallingConvention = CallingConvention.Winapi)]
#else
        [DllExport("_RVExtensionVersion@8", CallingConvention = CallingConvention.Winapi)]
#endif
        public static void RvExtensionVersion(StringBuilder output, int outputSize) {
            output.Append("1.0.0.0");
        }

        /// <summary>
        /// The entry point for the default callExtension command.
        /// </summary>
        /// <param name="output">The string builder object that contains the result of the function</param>
        /// <param name="outputSize">The maximum size of bytes that can be returned</param>
        /// <param name="function">The string argument that is used along with callExtension</param>
#if WIN64
        [DllExport("RVExtension", CallingConvention = CallingConvention.Winapi)]
#else
        [DllExport("_RVExtension@12", CallingConvention = CallingConvention.Winapi)]
#endif
        public static void RvExtension(StringBuilder output, int outputSize,
            [MarshalAs(UnmanagedType.LPStr)] string function) {
            outputSize--; // Ensure that we don't exceed the maximum output size - it's a bit paranoid but you should keep it there
            output.Append("This works!");
        }

        /// <summary>
        /// The entry point for the callExtensionArgs command.
        /// </summary>
        /// <param name="output">The string builder object that contains the result of the function</param>
        /// <param name="outputSize">The maximum size of bytes that can be returned</param>
        /// <param name="function">The string argument that is used along with callExtension</param>
        /// <param name="args">The args passed to callExtension as a string array</param>
        /// <param name="argsCount">The size of the string array args</param>
        /// <returns>The result code</returns>
#if WIN64
        [DllExport("RVExtensionArgs", CallingConvention = CallingConvention.Winapi)]
#else
        [DllExport("_RVExtensionArgs@20", CallingConvention = CallingConvention.Winapi)]
#endif
        public static int RvExtensionArgs(StringBuilder output, int outputSize,
            [MarshalAs(UnmanagedType.LPStr)] string function, 
            [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 4)] string[] args, int argCount) {
            outputSize--; // Ensure that we don't exceed the maximum output size - it's a bit paranoid but you should keep it there
            output.Append("This works!");

            return 100;
        }
}

The extension has to be build in x86 and x64 separately. A detailed instruction can be found here.

It's essentially important to use threading to prevent errors. If the extension is taking too much time to process a request it will be abandoned by Arma with an error code.

References