Jump to content
maca134

[How To] C# ARMA Extension

Recommended Posts

  1. First, if you have not got it already, download Visual Studio Community 2013 from here (if you dont have VS):
  2. Once installed, start a new C# “Class Library†project.
    Screenshot-at-00-57-38.png
  3. Next step is to download a NuGet Package called “Unmanaged Exports“. To do this goto the menu “Project -> Manage NuGet Packagesâ€, once the dialog opens, search for “Unmanaged Exportsâ€, install it and close the dialog.
    Screenshot-at-01-02-21.png
  4. Now, the project is setup and you are ready to writing some code. Here is the most basic of examples (Download link below).
    Screenshot-at-01-12-06.png
  5. The class itself can be called anything, DllEntry seems appropriate. Once you compile the code, the RVExtension gets called whenever you execute a callExtension. In the SQF code below, the function string, is available via the function param in RVExtension. (I had a string encoding issue, i thought maybe marshaling would help…not sure…).
    _output = "dllname" callExtension "function"; // _output == "noitcnuf"


  6. Before you can compile, the Unmanaged Exports requires the projects “Platform Target†to be set to either x86 or x64 (I suggest x86 if you are not sure). To do this right click on the project and select “Propertiesâ€, click the “Build†section and change the “Platform Target†dropdown.
    Screenshot-at-01-25-09.png
  7. Now compile the project and put the resulting DLL into an ARMA mod folder. (Eg. @armaExtCS). Thats it, to run it in ARMA just execute this:
    _output = "ARMAExtCS" callExtension "function";


Screenshot-at-01-28-24.png

Screenshot-at-01-29-37.png

More Information + Download

  • Like 2
  • Thanks 1

Share this post


Link to post
Share on other sites

As of Arma v1.67 there are some adjustments that have to be made when developing an extension.

 

First of all you have to make sure that the .dll is build with the prefix _x64.dll when the x64 platform is used.

You can do this in the Post-Build event with this code

IF $(PlatformName) == x64 (
    RENAME "$(TargetPath)" "$(TargetName)_x64$(TargetExt)"
)

 

The signature of the entry points are also differing.

 

When using the old RVExtension call you have to adjust the DllImport directive to make sure that the decorated name matches with the x64 version. 

#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)

 

When using the new RVExtensionArgs call you have to use this signature

#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)

The args are passed as a string array and can then be checked and parsed for further use. Please note that the return value is an int which represents a status code that can be defined by yourself.

  • Like 1
  • Thanks 1

Share this post


Link to post
Share on other sites
On ‎2‎/‎20‎/‎2017 at 5:38 AM, chris5790 said:

As of Arma v1.67 there are some adjustments that have to be made when developing an extension.

 

First of all you have to make sure that the .dll is build with the prefix _x64.dll when the x64 platform is used.

You can do this in the Post-Build event with this code


IF $(PlatformName) == x64 (
    RENAME "$(TargetPath)" "$(TargetName)_x64$(TargetExt)"
)

 

The signature of the entry points are also differing.

 

When using the old RVExtension call you have to adjust the DllImport directive to make sure that the decorated name matches with the x64 version. 


#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)

 

When using the new RVExtensionArgs call you have to use this signature


#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)

The args are passed as a string array and can then be checked and parsed for further use. Please note that the return value is an int which represents a status code that can be defined by yourself.

 

Hi, thank you for posting this. I tried to write an extension following maca134's article, and now it works in the 64-bit version. But it seems that the memory is slowly leaking in both x86 and x64 version of dll, even when it just parses the input and stores them to some static variables. Do you know how to fix this? I have searched about manually free memory in C# but didn't find any useful information.

Share this post


Link to post
Share on other sites
12 hours ago, tzuhanghz said:

 

Hi, thank you for posting this. I tried to write an extension following maca134's article, and now it works in the 64-bit version. But it seems that the memory is slowly leaking in both x86 and x64 version of dll, even when it just parses the input and stores them to some static variables. Do you know how to fix this? I have searched about manually free memory in C# but didn't find any useful information.

Hi,

 

memory leaking is a general concern when writing an extension in C#. You have to aware of the performance loss compared to a native extension/dll written in C# or C. 

 

C# handles this memory garbage with the inbuilt garbage collector which is different to C++ which hasn't one and has to be handled on your own. This also implies that you can't (or shouldn't) affect this garbage collector directly or indirectly. Direct calls to the garbage collector should be prevented when it is possible as this is affecting the whole garbage process and mostly has more negative effects than positive effects. Memory management is a huge topic when developing a C# app/library and can't be handled in a few sentences. There are a lot of techniques that ensure that memory is not leaking somewhere.

 

The problem you will be facing is that a C# extension is going to leak a tiny bit of memory, you can't change this behaviour. When exposing your extension to a native program you have to handle the native types that it is passing to it. This is done by marshalling (the MarshalAs part) which of course consumes memory. 

 

The C# garbage collector is build on top of a classification (heap) of memory usage and unused objects:

  • Generation 0: Short-lived objects like temporary variables.
  • Generation 1: The buffer containing objects that survived the first generation.
  • Generation 2: Long-lived objects like static data or big objects.

If an object is not used any more it will be marked as unused (disposed) or the garbage collector finds it. It is cycling through the whole memory that is allocated by the process and searches for dead objects that are not referenced any more. 

Any object that has been found this way goes to the first heap. There it is processed. If it is a generation 2 type it is then moved to the third heap.

Any object that survived the first garbage collection process goes to the second heap. There it is processed another time.

All objects in the third heap are processed like usual.

 

Such a process results in two different types of memory: freed memory which can be reused by the system and committed memory which can only be reused by the process. In the second case this memory is still used by the application but free for any internal use. You can't manually affect which case is going to be happen.

 

On top there is the LOH which contains any object that is equal or larger than 85,000 bytes. This could be a large array for example when downloading a file via the `WebClient` class (which is definitely not recommended as it results in memory leaks). This LOH is (normally) processed less frequently than the other garbage collections and is btw the only process you can force to run. But in general you shouldn't store big data on the heap (Streams are used to prevent such a heap). Any other process is done when needed. Of course you can force the garbage collector to run now but as I already outlined this is not recommended nor needed.

 

When trying to understand this whole procedure you have to be aware of the general memory usage. The memory usage will rise to a certain level. This is completely normal. If the usage of memory reaches a certain point the garbage collector will intervene and trying to restore memory.

 

If you are more interested into this process you can have a look at this msdn documentation. Be aware, this is a wall of text!

 

tl;dr  

This behaviour is completely normal. Only if the memory usage climbs a unusual manner you have to intervene and search for a potential leak. Static objects are known for using more memory than others because they are handled differently than normal objects (Generation 2 - shown above). Therefore they will lay in Generation 2 until they are not used any more. I'm quite sure that the memory consumption of your extension is just about some megabytes which is neither much or alarming.

 

There are some simple tips for reducing/managing the resources used by your application on the internet. They begin with simple things like using the StringBuilder for strings that are changing and end with complete high end solutions for smart memory managing. If you face any memory problems after implementing any features do a profiling to check what this is causing. And the leading tip: use a VCS! With it you can go through your changes and find the problem much easier.

Share this post


Link to post
Share on other sites

The latest implementation of CallExtension supports "RVExtensionVersion":

//--- Extension version information shown in .rpt file
void __stdcall RVExtensionVersion(char *output, int outputSize)
{
	//--- max outputSize is 32 bytes
	strncpy_s(output, outputSize, CURRENT_VERSION, _TRUNCATE);
}

I have 2 questions:

  1. For now: what would the 32-bit implementation in C# look like? It seems to need _ and @, and a number. 
  2. For the future: how can I find out what _method@number to use, if I had to find it out myself?

I hope you can help me out, thanks!

Share this post


Link to post
Share on other sites
On 12.6.2017 at 11:07 PM, Matthijs said:

The latest implementation of CallExtension supports "RVExtensionVersion":


//--- Extension version information shown in .rpt file
void __stdcall RVExtensionVersion(char *output, int outputSize)
{
	//--- max outputSize is 32 bytes
	strncpy_s(output, outputSize, CURRENT_VERSION, _TRUNCATE);
}

I have 2 questions:

  1. For now: what would the 32-bit implementation in C# look like? It seems to need _ and @, and a number. 
  2. For the future: how can I find out what _method@number to use, if I had to find it out myself?

I hope you can help me out, thanks!

Normally they are documented here: https://community.bistudio.com/wiki/Extensions

 

If not, the general naming rule for the decorated name is for 32 bit:

_<functionName>@<argsLength>

 

Args length is the length in bytes. A char pointer is 4 and an int is 4 too so the decorated name should be _RVExtensionVersion@8

 

I added a sample demo code and some instructions to the BI wiki. I will extend it in the future if new entry points are added or a detailed instruction is needed.

 

  • Like 1

Share this post


Link to post
Share on other sites

 

On 15-6-2017 at 11:29 PM, chris5790 said:

Normally they are documented here: https://community.bistudio.com/wiki/Extensions

 

If not, the general naming rule for the decorated name is for 32 bit:

_<functionName>@<argsLength>

 

Args length is the length in bytes. A char pointer is 4 and an int is 4 too so the decorated name should be _RVExtensionVersion@8

 

I added a sample demo code and some instructions to the BI wiki. I will extend it in the future if new entry points are added or a detailed instruction is needed.

 

Thanks! :icon_biggrin:

  • Like 1

Share this post


Link to post
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now

×