Extensions: Difference between revisions

From Bohemia Interactive Community
Jump to navigation Jump to search
m (Markup fix)
(Fully Rewrote Page)
Line 1: Line 1:
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.
{{warning|If you came here because you have been told to install an extension, be warned! Extensions are just like executables. They may contain viruses etc. More on that, in the [[#FAQ]] section}}


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


==For players==
__TOC__
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.
=Historical=
This functionality arrived in an ArmA 2 post-1.60 beta, making 1.61 the first stable version to support it. Linux (server) support was not available until [[:Category:Introduced_with_Arma_3_version_1.08|Arma 3 v1.18]].


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.)
=FAQ=
====How to use Extensions?====
Extensions can be accessed using [[callExtension]].


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.
''The exact way about how to use an extension is up to the developer of said extension''


=== Defending against malicious extensions ===
====Where to put Extensions?====
Arma will look at the following places to find your extension files:
* In all loaded mods root folders (for example: ''..\arma3\@ModXYZ\yourextension.dll'').
* In your arma installation folder (for example: ''..\arma3\yourextension.dll'').


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.
At best, you place your extensions inside some mod folder and load it.
At worst, you just throw it where the arma executable is located.


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.
''Extensions may require other libraries and runtimes to be installed on the computer it is to run on.''
Consult the authors installation manual if it exists.


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.)
====What are Extensions?====
Extensions are ''Dynamic Link Library'' (DLL) files written in, for example, C.


Linux systems use a different file type: ''Shared Object'' (SO).


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.
====What do i have to look out for?====
Extensions fall under the same rules as executable files.
They may require additional runtime libraries or cause the game to crash.


=== Defending against vulnerable extensions ===
Worst case: They can contain malicious code.


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.
====How to know which extensions can be trusted?====
Theoretically, never trust anything! Including Arma and your OS itself.
Technically however, this would make a boring experience, as all you would be left with is an expensive heater.


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.)
Thus a better rule is: If the are sources freely available, you probably can trust the extension.
Even if you cannot read the source code, chances are that somebody out there can and would find something odd.
That will not protect you!
Developers might add malicious code that is not exposed in the available source files, they not even need to be aware of this.
They may be infected already and the virus is just writing itself into the compiled extension.


==For server admins==
====When i call the extension method, Arma freezes.====
This is most likely caused by the extension taking too much time.
Due to the low-level nature of extensions, an extension cannot be suspended like [[Scheduler|scheduled]] scripts can be.
You should notify the developer of said extension about this freezing issue or use the extension less extensively.


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.  
====Can i join BattlEye protected servers with my extension?====
Many users will blindly follow your instructions, which makes the ''for users'' section content even more important for you.
If the extension is whitelisted by BattlEye, it will be allowed to load.
If it is not, it just will be blocked causing any [[callExtension]] attempt to fail.


And we haven't even mentioned using extensions on the server.
Generally speaking, joining BattlEye protected servers should be no problem with or without extensions.
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.
If in doubt, ask the extensions author.


==For mission makers==
====Can i use extensions in my mission?====
Yes, you can use extensions in your missions, but you should consider if it is worth it.
Using extensions requires that all hosts (servers and clients) which use the extension have it installed.
Important to know here: If only the server is using the extension, the clients do not need it.


A few notes in no particular order
Extensions are not packed into the mission pbo.


* 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.
=Creating Extensions=
* 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.
==Preamble==
* 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.)
Before we start, ask yourself the following questions:
* is SQF unable to fulfill your needs?
* Making an extension requires knowledge of a programming language. Are you capable of performing that programming task?
* Why should a user trust you and use your extension?
* Is it worth adding an extension for the feature you want to implement?


=== Scripting ===
If the answer to any question was no, you probably should not proceed to create an extension but rather use plain SQF.
See [[callExtension]].
The exact content of the strings will depend entirely on the extension in use.


== For addon makers ==
==Getting Started==
At first, we have to choose what language we want to use.
<!-- If you plan on adding another language, make sure you can provide code snippets for ALL examples required! -->
This wiki currently covers the following: '''C''', '''C++''', '''C#'''


Extensions are distributed as part of an addon.
===Preparations===
Thus this section will end up being rather expansive.
====C/C++====
Create a new library project in the IDE of your choice.


=== A few considerations ===
====C#====
C# requires you to install additional dependencies to work out of the box.
The snippets in here all are using [https://www.nuget.org/packages/UnmanagedExports DllExport] ([https://github.com/3F/DllExport sources]) which can be installed using NuGet.


* Can what you're trying to achieve be done in pure sqf?
Create a new Class Library project in the IDE of your choice.
* 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 ===
==Available Interfaces==
The Extension is required to contain at least the entry point in a form of a method named RVExtension or RVExtensionArgs.
Everything else is optional.


* 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.)
The actual exports need to look like this (Windows only):
* 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 ===
'''32-bit'''
* {{Inline code|_RVExtension@12}}
* {{Inline code|_RVExtensionArgs@20}}
* {{Inline code|_RVExtensionVersion@8}}
'''64-bit'''
* {{Inline code|RVExtension}}
* {{Inline code|RVExtensionArgs}}
* {{Inline code|RVExtensionVersion}}


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.  
If you use the Visual C++ compiler, this should work out of the box. But other compilers might require some manual work.
===C/C++===
====Visual C++====
<source lang="c">
//--- Called by Engine on extension load
__declspec (dllexport) void __stdcall RVExtensionVersion(char *dest, int num);
//--- STRING callExtension STRING
__declspec (dllexport) void __stdcall RVExtension(char *dest, int num, const char *fnc);
//--- STRING callExtension ARRAY
__declspec (dllexport) int __stdcall RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
</source>
====GCC====
<source lang="c">
//--- Called by Engine on extension load
__attribute__((dllexport)) void RVExtensionVersion(char *dest, int num);
//--- STRING callExtension STRING
__attribute__((dllexport)) void RVExtension(char *dest, int num, const char *fnc);
//--- STRING callExtension ARRAY
__attribute__((dllexport)) int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
</source>
===C#===
<source lang="c#">
        /// <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) { }


(TODO: Insert a couple example projects)
        /// <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) { }


====DLL Interface====
        /// <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) { }
</source>
 
==Getting your extension whitelisted by BattlEye==
Since Arma 3 1.46, extensions need to be whitelisted in order to get loaded.


The DLL is expected to contain an entry point in a form of a function named RVExtension or RVExtensionArgs (exported with [https://msdn.microsoft.com/en-us/library/56h2zst2.aspx decorated] name <tt>_RVExtension@12</tt> / <tt>_RVExtensionArgs@20</tt>  / <tt>_RVExtensionVersion@8</tt> for 32-bit extension and <tt>RVExtension</tt> / <tt>RVExtensionArgs</tt> / <tt>RVExtensionVersion</tt> 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:
The [https://dev.arma3.com/post/sitrep-00109 SitRep #00109] said the following on that topic:
  The enhanced BattlEye protection in 1.46 will require extensions to be '''white-listed''', or their access to the game's processes will be blocked.
  In order to get your extension white-listed, please contact BattlEye support via [http://www.battleye.com/contact/ its web form] or support[at]battleye.com.
  Be sure to include the file name, a description and possibly a download link.


<source lang="c">
This boils down to the following tasks to get your extension whitelisted:
void __stdcall RVExtension(char *output, int outputSize, const char *function);
# Upload the extension to whitelist somewhere.
int __stdcall RVExtensionArgs(char *output, int outputSize, const char *function, const char **args, int argCnt);
# Open the [http://www.battleye.com/contact/ BattlEye Contact Page].
# Pick the topic {{Inline code|Other requests}} (may change in future, select most appropriate).
# Fill in your Name and E-Mail.
# Set the subject to something like {{Inline code|Arma extension whitelisting}}.
# Add link to your extension from step 1 (you may include other informations too) into the Message.
 
After a certain time, your extension should be whitelisted.
 
==Minimum Viable Example==
The following examples will copy the input into the output. They are provided to show some practical example.
===C===
====Visual C++====
{|class="wikitable"
!|myextension.c
|-
| <source lang="c">
__declspec(dllexport) void __stdcall RVExtension(char *dest, int num, const char *fnc);
__declspec(dllexport) int __stdcall RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
__declspec(dllexport) void __stdcall RVExtensionVersion(char *dest, int num);
 
int strncpy_safe(char *dest, const char *src, int size)
{
int i;
size--;
for (i = 0; i < size && src[i] != '\0'; i++)
{
dest[i] = src[i];
}
dest[i] = '\0';
return i;
}
 
void RVExtension(char *dest, int num, const char *fnc)
{
strncpy_safe(dest, function, num);
}
 
int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc)
{
int index = 0;
int i;
for (i = 0; i < argc && index < num; i++)
{
index += strncpy_safe(dest + index, argv[i], num - 1 - index);
}
return 0;
}
 
void RVExtensionVersion(char *dest, int num)
{
strncpy_safe(dest, "Test-Extension v1.0", num);
}
</source>
</source>
|}
====GCC====
{|class="wikitable"
!|myextension.c
|-
| <source lang="c">
__attribute__((dllexport)) void RVExtension(char *dest, int num, const char *fnc);
__attribute__((dllexport)) int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
__attribute__((dllexport)) void RVExtensionVersion(char *dest, int num);


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.
int strncpy_safe(char *dest, const char *src, int size)
{
int i;
size--;
for (i = 0; i < size && src[i] != '\0'; i++)
{
dest[i] = src[i];
}
dest[i] = '\0';
return i;
}


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 [http://msdn.microsoft.com/en-us/library/windows/desktop/dd319072%28v=vs.85%29.aspx MultiByteToWideChar] and [http://msdn.microsoft.com/en-us/library/windows/desktop/dd374130%28v=vs.85%29.aspx WideCharToMultiByte] functions.
void RVExtension(char *dest, int num, const char *fnc)
{
strncpy_safe(dest, function, num);
}


The example dll source example follows. In this example the dll simply copies the input to the output:
int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc)
{
int index = 0;
int i;
for (i = 0; i < argc && index < num; i++)
{
index += strncpy_safe(dest + index, argv[i], num - 1 - index);
}
return 0;
}


<source lang="c">
void RVExtensionVersion(char *dest, int num)
// dllmain.cpp : Defines the entry point for the DLL application.
{
#define WIN32_LEAN_AND_MEAN            // Exclude rarely-used stuff from Windows headers
strncpy_safe(dest, "Test-Extension v1.0", num);
// Windows Header Files:
}
#include <windows.h>
</source>
|}
===C++===
====Visual C++====
{|class="wikitable"
!|myextension.cpp
|-
| <source lang="c++">
#include <string>
#include <string>
#include <sstream>
extern "C"
{
__declspec(dllexport) void __stdcall RVExtension(char *dest, int num, const char *fnc);
__declspec(dllexport) int __stdcall RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
__declspec(dllexport) void __stdcall RVExtensionVersion(char *dest, int num);
}
void RVExtension(char *dest, int num, const char *fnc)
{
strncpy(dest, fnc, num);
dest[num] = '\0';
}


BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc)
{
{
switch (ul_reason_for_call)
std::stringstream sstream;
for (i = 0; i < argc; i++)
{
{
case DLL_PROCESS_ATTACH:
sstream << argv[i];
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
}
return TRUE;
strncpy(dest, sstream.str(), num - 1);
dest[num - 1] = '\0';
return 0;
}
}


void RVExtensionVersion(char *dest, int num)
{
strncpy(dest, "Test-Extension v1.0", num - 1);
dest[num - 1] = '\0';
}
</source>
|}
====GCC====
{|class="wikitable"
!|myextension.cpp
|-
| <source lang="c++">
#include <string>
#include <sstream>
extern "C"
extern "C"
{
{
  __declspec(dllexport) void __stdcall RVExtension(char *output, int outputSize, const char *function);
__attribute__((dllexport)) void RVExtension(char *dest, int num, const char *fnc);
  __declspec(dllexport) int __stdcall RVExtensionArgs(char *output, int outputSize, const char *function, const char **args, int argCnt); // Experimental
__attribute__((dllexport)) int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
};
__attribute__((dllexport)) void RVExtensionVersion(char *dest, int num);
}
void RVExtension(char *dest, int num, const char *fnc)
{
strncpy(dest, fnc, num);
dest[num] = '\0';
}


void __stdcall RVExtension(char *output, int outputSize, const char *function)
int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc)
{
{
  outputSize -= 1;
std::stringstream sstream;
  strncpy(output,function,outputSize);
for (i = 0; i < argc; i++)
{
sstream << argv[i];
}
strncpy(dest, sstream.str(), num - 1);
dest[num - 1] = '\0';
return 0;
}
}


int __stdcall RVExtensionArgs(char *output, int outputSize, const char *function, const char **args, int argCnt) // Experimental
void RVExtensionVersion(char *dest, int num)
{
{
  outputSize -= 1;
strncpy(dest, "Test-Extension v1.0", num - 1);
  std::string out = function + ":";
dest[num - 1] = '\0';
  for (int i = 0; i < argCnt; i++)
  {
    out += args[i];
    out += ', ';
  }
  strncpy(output,function,out.c_str());
}
}
</source>
</source>
|}
===C#===
{|class="wikitable"
!|MyExtension.cs
|-
| <source lang="c#">
using System.Runtime.InteropServices;


A starter tutorial on how to make ArmA extension in '''C++''' using Visual Studio Express 2012 can be found [http://killzonekid.com/arma-scripting-tutorials-how-to-make-arma-extension-part-1/ here (part1)] and [http://killzonekid.com/arma-scripting-tutorials-how-to-make-arma-extension-part-2/ here (part2)].
class MyExtension {


A starter tutorial on how to make ArmA extension in '''C#''' using Visual Studio Express 2013 can be found [http://killzonekid.com/arma-scripting-tutorials-how-to-make-arma-extension-part-3/ here (part3)]
#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("Test-Extension v1.0");
        }
 
#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)
{
            output.Append(function);
        }
 
#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)
{
foreach(var arg in args)
{
output.Append(arg);
}
            return 0;
        }
}
</source>
|}
==Other Languages==
This section contains other languages which may or may not work. Finding out requirements etc. is up to you.
{{warning|Help Wanted}}
If you know any of the following languages, consider editing this page to add the language to the upper list properly.
===Delphi/Pascal===
{|class="wikitable"
!|&lt;unknown&gt;
|-
| <source lang="delphi">
library dllTest;


A threaded extension example in '''C++''' using Visual Studio Express 2013 can be found [http://killzonekid.com/arma-scripting-tutorials-how-to-make-arma-extension-part-4/ here (part4)]
uses
  SysUtils;


Another example written in '''C#''' using Visual Studio Express 2013 and without having to use '''C++/CLR''' can be found [http://maca134.co.uk/tutorial/write-an-arma-extension-in-c-sharp-dot-net/ here]
{Return value is not used.}
procedure RVExtension(toArma: PAnsiChar; outputSize: Integer; fromArma: PAnsiChar); stdcall; export;
begin
  StrCopy(toArma, fromArmA);
end;


Another example, written in the D programming language:
exports
<source lang="d">
  { 32-bit }
  RVExtension name '_RVExtension@12';
  { 64-bit }
  RVExtension name 'RVExtension';
begin
end.
</source>
|}
===D===
{|class="wikitable"
!|&lt;unknown&gt;
|-
| <source lang="d">
import std.c.windows.windows;
import std.c.windows.windows;
import core.sys.windows.dll;
import core.sys.windows.dll;
Line 189: Line 480:
doutput[0 .. result.length] = result[];
doutput[0 .. result.length] = result[];
doutput[result.length] = '\0';
doutput[result.length] = '\0';
}</source>
Another example, written in Delphi/Pascal:
<source lang="delphi">
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.</source>
==== Linux shared library interface ====
Extension support was added to Linux in Arma 3 v1.18. The shared library should export the following function:
<source lang="c">
void RVExtension(char *output, int outputSize, const char *function);
</source>
Here's an example of a simple shared library that copies the input to the output, just like the Windows DLL example above:
<source lang="c">
/*
* 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;
}
}
</source>
</source>
|}


====Extension existence/version checking====
=External References=
 
====C/C++====
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.
* [http://killzonekid.com/arma-scripting-tutorials-how-to-make-arma-extension-part-1/ ArmA Scripting Tutorials: How To Make ArmA Extension (Part 1)]
 
* [http://killzonekid.com/arma-scripting-tutorials-how-to-make-arma-extension-part-2/ ArmA Scripting Tutorials: How To Make ArmA Extension (Part 2)]
<source lang="c">
* [http://killzonekid.com/arma-scripting-tutorials-how-to-make-arma-extension-part-3/ ArmA Scripting Tutorials: How To Make ArmA Extension (Part 3)]
void __stdcall RVExtension(char *output, int outputSize, const char *function)
* [http://killzonekid.com/arma-scripting-tutorials-how-to-make-arma-extension-part-4/ ArmA Scripting Tutorials: How To Make ArmA Extension (Part 4)]
{
====C#====
  outputSize -= 1;
* [http://maca134.co.uk/tutorial/write-an-arma-extension-in-c-sharp-dot-net/ Write An ARMA Extension In C#/.NET]
  if (!strcmp(function,"version"))
  {
    strncpy(output,"1.0",outputSize);
  }
  else
  {
    strncpy(output,function,outputSize);
  }
}
</source>
 
=== 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 [https://github.com/3F/DllExport DllExport] or [https://www.nuget.org/packages/UnmanagedExports/1.2.2.23707 Unmanaged Exports].
 
The entry points can be in any class, this doesn't matters. This is the minimal example of a working C# extension:
 
<source lang="csharp">
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;
        }
}
</source>
 
The extension has to be build in x86 and x64 separately. A detailed instruction can be found [http://maca134.co.uk/tutorial/write-an-arma-extension-in-c-sharp-dot-net/ 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==
<references/>

Revision as of 13:28, 7 May 2018

If you came here because you have been told to install an extension, be warned! Extensions are just like executables. They may contain viruses etc. More on that, in the #FAQ section


Historical

This functionality arrived in an ArmA 2 post-1.60 beta, making 1.61 the first stable version to support it. Linux (server) support was not available until Arma 3 v1.18.

FAQ

How to use Extensions?

Extensions can be accessed using callExtension.

The exact way about how to use an extension is up to the developer of said extension

Where to put Extensions?

Arma will look at the following places to find your extension files:

  • In all loaded mods root folders (for example: ..\arma3\@ModXYZ\yourextension.dll).
  • In your arma installation folder (for example: ..\arma3\yourextension.dll).

At best, you place your extensions inside some mod folder and load it. At worst, you just throw it where the arma executable is located.

Extensions may require other libraries and runtimes to be installed on the computer it is to run on. Consult the authors installation manual if it exists.

What are Extensions?

Extensions are Dynamic Link Library (DLL) files written in, for example, C.

Linux systems use a different file type: Shared Object (SO).

What do i have to look out for?

Extensions fall under the same rules as executable files. They may require additional runtime libraries or cause the game to crash.

Worst case: They can contain malicious code.

How to know which extensions can be trusted?

Theoretically, never trust anything! Including Arma and your OS itself. Technically however, this would make a boring experience, as all you would be left with is an expensive heater.

Thus a better rule is: If the are sources freely available, you probably can trust the extension. Even if you cannot read the source code, chances are that somebody out there can and would find something odd. That will not protect you! Developers might add malicious code that is not exposed in the available source files, they not even need to be aware of this. They may be infected already and the virus is just writing itself into the compiled extension.

When i call the extension method, Arma freezes.

This is most likely caused by the extension taking too much time. Due to the low-level nature of extensions, an extension cannot be suspended like scheduled scripts can be. You should notify the developer of said extension about this freezing issue or use the extension less extensively.

Can i join BattlEye protected servers with my extension?

If the extension is whitelisted by BattlEye, it will be allowed to load. If it is not, it just will be blocked causing any callExtension attempt to fail.

Generally speaking, joining BattlEye protected servers should be no problem with or without extensions.

If in doubt, ask the extensions author.

Can i use extensions in my mission?

Yes, you can use extensions in your missions, but you should consider if it is worth it. Using extensions requires that all hosts (servers and clients) which use the extension have it installed. Important to know here: If only the server is using the extension, the clients do not need it.

Extensions are not packed into the mission pbo.

Creating Extensions

Preamble

Before we start, ask yourself the following questions:

  • is SQF unable to fulfill your needs?
  • Making an extension requires knowledge of a programming language. Are you capable of performing that programming task?
  • Why should a user trust you and use your extension?
  • Is it worth adding an extension for the feature you want to implement?

If the answer to any question was no, you probably should not proceed to create an extension but rather use plain SQF.

Getting Started

At first, we have to choose what language we want to use.

This wiki currently covers the following: C, C++, C#

Preparations

C/C++

Create a new library project in the IDE of your choice.

C#

C# requires you to install additional dependencies to work out of the box. The snippets in here all are using DllExport (sources) which can be installed using NuGet.

Create a new Class Library project in the IDE of your choice.

Available Interfaces

The Extension is required to contain at least the entry point in a form of a method named RVExtension or RVExtensionArgs. Everything else is optional.

The actual exports need to look like this (Windows only):

32-bit

  • _RVExtension@12
  • _RVExtensionArgs@20
  • _RVExtensionVersion@8

64-bit

  • RVExtension
  • RVExtensionArgs
  • RVExtensionVersion

If you use the Visual C++ compiler, this should work out of the box. But other compilers might require some manual work.

C/C++

Visual C++

	//--- Called by Engine on extension load 
	__declspec (dllexport) void __stdcall RVExtensionVersion(char *dest, int num);
	//--- STRING callExtension STRING
	__declspec (dllexport) void __stdcall RVExtension(char *dest, int num, const char *fnc);
	//--- STRING callExtension ARRAY
	__declspec (dllexport) int __stdcall RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);

GCC

	//--- Called by Engine on extension load 
	__attribute__((dllexport)) void RVExtensionVersion(char *dest, int num);
	//--- STRING callExtension STRING
	__attribute__((dllexport)) void RVExtension(char *dest, int num, const char *fnc);
	//--- STRING callExtension ARRAY
	__attribute__((dllexport)) int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);

C#

        /// <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) { }

        /// <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) { }

        /// <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) { }

Getting your extension whitelisted by BattlEye

Since Arma 3 1.46, extensions need to be whitelisted in order to get loaded.

The SitRep #00109 said the following on that topic:

 The enhanced BattlEye protection in 1.46 will require extensions to be white-listed, or their access to the game's processes will be blocked.
 In order to get your extension white-listed, please contact BattlEye support via its web form or support[at]battleye.com.
 Be sure to include the file name, a description and possibly a download link.

This boils down to the following tasks to get your extension whitelisted:

  1. Upload the extension to whitelist somewhere.
  2. Open the BattlEye Contact Page.
  3. Pick the topic Other requests (may change in future, select most appropriate).
  4. Fill in your Name and E-Mail.
  5. Set the subject to something like Arma extension whitelisting.
  6. Add link to your extension from step 1 (you may include other informations too) into the Message.

After a certain time, your extension should be whitelisted.

Minimum Viable Example

The following examples will copy the input into the output. They are provided to show some practical example.

C

Visual C++

myextension.c
__declspec(dllexport) void __stdcall RVExtension(char *dest, int num, const char *fnc);
__declspec(dllexport) int __stdcall RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
__declspec(dllexport) void __stdcall RVExtensionVersion(char *dest, int num);

int strncpy_safe(char *dest, const char *src, int size)
{
	int i;
	size--;
	for (i = 0; i < size && src[i] != '\0'; i++)
	{
		dest[i] = src[i];
	}
	dest[i] = '\0';
	return i;
}

void RVExtension(char *dest, int num, const char *fnc)
{
	strncpy_safe(dest, function, num);
}

int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc)
{
	int index = 0;
	int i;
	for (i = 0; i < argc && index < num; i++)
	{
		index += strncpy_safe(dest + index, argv[i], num - 1 - index);
	}
	return 0;
}

void RVExtensionVersion(char *dest, int num)
{
	strncpy_safe(dest, "Test-Extension v1.0", num);
}

GCC

myextension.c
__attribute__((dllexport)) void RVExtension(char *dest, int num, const char *fnc);
__attribute__((dllexport)) int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
__attribute__((dllexport)) void RVExtensionVersion(char *dest, int num);

int strncpy_safe(char *dest, const char *src, int size)
{
	int i;
	size--;
	for (i = 0; i < size && src[i] != '\0'; i++)
	{
		dest[i] = src[i];
	}
	dest[i] = '\0';
	return i;
}

void RVExtension(char *dest, int num, const char *fnc)
{
	strncpy_safe(dest, function, num);
}

int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc)
{
	int index = 0;
	int i;
	for (i = 0; i < argc && index < num; i++)
	{
		index += strncpy_safe(dest + index, argv[i], num - 1 - index);
	}
	return 0;
}

void RVExtensionVersion(char *dest, int num)
{
	strncpy_safe(dest, "Test-Extension v1.0", num);
}

C++

Visual C++

myextension.cpp
#include <string>
#include <sstream>
extern "C"
{
	__declspec(dllexport) void __stdcall RVExtension(char *dest, int num, const char *fnc);
	__declspec(dllexport) int __stdcall RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
	__declspec(dllexport) void __stdcall RVExtensionVersion(char *dest, int num);
}
void RVExtension(char *dest, int num, const char *fnc)
{
	strncpy(dest, fnc, num);
	dest[num] = '\0';
}

int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc)
{
	std::stringstream sstream;
	for (i = 0; i < argc; i++)
	{
		sstream << argv[i];
	}
	strncpy(dest, sstream.str(), num - 1);
	dest[num - 1] = '\0';
	return 0;
}

void RVExtensionVersion(char *dest, int num)
{
	strncpy(dest, "Test-Extension v1.0", num - 1);
	dest[num - 1] = '\0';
}

GCC

myextension.cpp
#include <string>
#include <sstream>
extern "C"
{
	__attribute__((dllexport)) void RVExtension(char *dest, int num, const char *fnc);
	__attribute__((dllexport)) int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc);
	__attribute__((dllexport)) void RVExtensionVersion(char *dest, int num);
}
void RVExtension(char *dest, int num, const char *fnc)
{
	strncpy(dest, fnc, num);
	dest[num] = '\0';
}

int RVExtensionArgs(char *dest, int num, const char *fnc, const char **argv, int argc)
{
	std::stringstream sstream;
	for (i = 0; i < argc; i++)
	{
		sstream << argv[i];
	}
	strncpy(dest, sstream.str(), num - 1);
	dest[num - 1] = '\0';
	return 0;
}

void RVExtensionVersion(char *dest, int num)
{
	strncpy(dest, "Test-Extension v1.0", num - 1);
	dest[num - 1] = '\0';
}

C#

MyExtension.cs
using System.Runtime.InteropServices;

class MyExtension {

#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("Test-Extension v1.0");
        }

#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)
		{
            output.Append(function);
        }

#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)
		{
			foreach(var arg in args)
			{
				output.Append(arg);
			}
            return 0;
        }
}

Other Languages

This section contains other languages which may or may not work. Finding out requirements etc. is up to you.

Help Wanted

If you know any of the following languages, consider editing this page to add the language to the upper list properly.

Delphi/Pascal

<unknown>
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.

D

<unknown>
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';
}

External References

C/C++

C#