Skip to content
⌘ NSIS Forum Archive

Calling managed .NET DLL from NSIS - this works :-)

112 posts

claesabrandt#edited

Calling managed .NET DLL from NSIS - this works :-)

Hi there,

This is a howto on calling managed .NET DLLs from NSIS. It works and is very easy.

Background:

One thing you should know is, that when you call a .NET DLL from NSIS, your installer will be dependent upon that the .NET framework is installed. The NSIS documentation contains information on how to detect the presence of the framework and how to download and install it if nessecary.

If you have a C# DLL, it requires that you write a managed .NET C++/CLR wrapper. You do not need to write a C++ Win32 wrapper, which is not so funny when you want to call your managed C# DLL. In your wrapper you will be able to call your C# code directly.

Setting up your project:

I assume you already have your C# DLL ready with some classes and methods in it you want to call. If you have this in a solution, you can now add a new project to the solution, select C++ CLR, then select class library. This will generate a managed C++/CLR dll and this is the DLL you will be calling from NSIS. Remember to add your C# project as a reference in your C++/CLR project.

The C++/CLR code:

Alright. What you have to do now is to write a C++/CLR funtion for each C# method you want to call. The C++/CLR function should look like this in your cpp or h file:

#pragma once // placed at the top of the file
extern "C" __declspec(dllexport) char* SomeFunction()
{
  // get a string from your C# method (in this case it is a static method)
  System::String ^str = MyNamespace::MyClass::MyMethod();
  // convert string to char* (this is one line)
  char* chp = (char*)(void*)System::Runtime::InteropServices::Marshal::StringToHGlobalAnsi(str);
  // return the char* to NSIS
  return chp;
} 
That's it 🙂 To call the function from NSIS:
SetOutPath $PLUGINSDIR
System::Call 'DLLName::SomeFunction(v) t .r0' 
This will return the string in register $0. You can have parameters too. See the NSIS documentation for more information on System::Call.

Make sure to include both the C++/CLR dll and the C# dll in your installer and place them in for instance the $PLUGINSDIR. When calling the C++/CLR dll with System::Call, be sure to make a call to SetOutPath $PLUGINSDIR first, so NSIS can find the DLLs.

Hope you can use this.

Regards Claes Brandt
claesabrandt#
Done. It is now located here:
http://nsis.sourceforge.net/Calling_Managed_.Net_DLL_From_NSIS
claesabrandt#
I justed tested my installer on a system with no .NET framework at all, and it seems I have to make sure that both the .NET framework and Visual C++ redistributable are installed. I'm gonna write this into the wiki.
claesabrandt#
Forget that. I found it to be something else. Basically it is because the cannot find the C# dll when calling it via the C++ dll. I will work on it and get back when I have the solution.
Afrow UK#
Try setting the current directory to $PLUGINSDIR with SetOutPath. If that doesn't work you could try updating the PATH variable to point to $PLUGINSDIR. Maybe it would be easier just to extract the C# dll to $SYSDIR.

Stu
claesabrandt#
Unfortunately none of those three method works, I tried them. It would have been nice. I will explain at the bottom of this post why and some details, but here is a workaround, that will work:

1) Create your script that calls your C++/CLR DLL which then in turn calls your C# DLL. This is what I described in the first post. But do not copy your C# DLL file in this script. Resulting exe could be for instance setup1.exe

2) Create another script that includes your C# DLL and setup1.exe. In .onGUIInit it copies them to $TEMP, then executes setup1.exe. When setup1.exe runs and calls the C++/CLR DLLs, which in turn calls the C# DLLs, the .NET framework can find the C# DLLs. When setup1.exe finishes, you delete both the C# DLL and setup1.exe then call abort. You will never see the first installer.

I guess I should include this in the Wiki page?

---

Here is why it will not work trying to set the current dir, setting the path or extract the C# dll to $SYSDIR: When the NSIS installer invokes those C++/CLR DLL functions, .NET assumes it should look for the C# DLLs in the same directory as the installer exe.

Now, we could extract the C# DLL in the same directory as where the installer exe runs, but if it runs from a readonly directory, it extract will fail.

There are ways to make .NET probe for DLLs in other directories than where the executing exe is located. This MSDN article explains it: http://msdn.microsoft.com/en-us/library/15hyw9x3(VS.71).aspx. The problem in those methods is that we still cannot copy a config file to a readonly directory.

We could also use reflection to load the C# assembly from any location we want, but then we loose compile-time error catching, and loading the assembly like this is ugly and I had some trouble getting NSIS to release the assembly file once it was loaded - it simply couldn't delete the DLL when exiting the installer.

Claes
claesabrandt#
I have work in progress 🙂 This time it is a real NSIS plug-in, that can load a .NET DLL. I guess it will be called CLR.dll or CLRLoader.dll or something. What I have working so far:

1) It is placed in the plugins dir in NSIS.
2) It is called with (in one line):
CLRLoader::Invoke "SomeAssembly.dll" "SomeNamespace.SomeClass" "SomeMethod" "param1,param2,param3"
pop $0 
3) It can handle string parameters and string return value at the moment.

Todo: Dynamic number of parameters.
Todo: Object type return value.
Todo: Fix a bug where that causes the DLL to remain in the tmp directory. NSIS can't delete it.

I also have it in a version, where you call it with System::Call, and this version does not remain in the tmp directory. This version has to be included manually in the installer and placed in for instance $PLUGINSDIR.

It is called with (one line):
System::Call 'CLRLoader::Invoke(t "SomeAssembly.dll", t "SomeNamespace.SomeClass", t "SomeMethod", t "param1,param2,param3") t .r0' 
Both versions are attached.

If you want to try it, remember to rename the desired DLL to CLRLoader.dll. If it is the "native" NSIS version, put it in the pluginsdir.
claesabrandt#edited
My new NSIS plug-in is now working. It can call methods in managed .NET DLLs. It supports dynamic number of parameters of types void, string (unicode), int, float, bool. Return value can be these types too. Strings and floats must be enclosed in "". The .NET classes called must have a default constructor.

You can find the plug-in attached to this post. This is version 0.1 of the plugin. I will write a wiki plug-in page for it shortly. It is provided here as-is for testing.

Let me know what you think of it, if you have any suggestions, questions or have trouble using it. Also, if you think more types should be supported for input and return values, let me know.

The following is a sample script that copies a .NET dll to pluginsdir along with this plugin. The .NET method called takes the following parameters: string, int, float and boolean. It then returns a string.
SetOutPath $PLUGINSDIR
File "SomeAssembly.dll" ; some test .NET assembly
File "CLR.dll" ; this plugin
System::Call `CLR::Call(w 'SomeAssembly.dll::SomeNamespace::SomeClass::SomeMethod("some string value",12,"15,8",false)') w .r0` 
This replaces the above method I described and my former wiki, as this plug-in fixes some issues with the method described previously.

Regards Claes

P.S.: Minor issue: You have to call it via System::Call. While this works perfectly, I would like it to be a true NSIS plugin. The reason for why it is called via System::Call is that if I make it a real NSIS plugin, the DLL remains in the $PLUGINS dir when the installer closes. If I get to fix that issue, I can make it a real NSIS plugin, that does not need System::Call.
claesabrandt#
Stu, I have a question. I really would like this to be a native NSIS dll, but as stated, the dll will not be deleted by NSIS, when the installer closes. I found out that it is automatically marked by NSIS to be deleted along with the directory, on next reboot, and so the file is gone. My question is, would that be ok? Or should I for now stick to the version where I call it with System::Call? The call would be much cleaner, if I made it a native NSIS plugin, and I probably would devide the parameters up into different parts. And the user would not have to include it manually, just put it in the plugins dir.
claesabrandt#
Stu, would it be possible to remove the zip file from post #8, as it seems some have downloadet that instead of the zip in post #9. The zip in post #8 should not be used.
Afrow UK#
Sorry I just deleted the wrong one. Either way it would be best to upload to the Wiki instead.

Upload to for example:


Then to embed in your page:
<attach>CLRLoader.zip</attach>

Stu
claesabrandt#
New wiki page including download

No problem 🙂 I just made a new wiki page at http://nsis.sourceforge.net/Call_.NET_DLL_methods_plug-in, including the download. It is located under plug-ins.
claesabrandt#
Yes, in Denmark we use a comma there. I know that you use dot in UK and US, so I will correct the sample. However, I am working on another way to pass in parameters (using variable arguments in c++), which will eliminate the problem.
rbchasetfb#
Love the CLRLoader...did just what I needed. However, is there a trick to get the CLRLoader.dll to delete from the $PLUGINSDIR when the NSIS Setup ends. All the other DLLs and files delete okay, including the one that CLRLoader calls, just the CLRLoader.dll is left behind. Here's the code snipet:

SetPluginUnload  alwaysoff
Function loadDll
    InitPluginsDir
    SetOutPath $PLUGINSDIR
    File "${NSISDIR}\\Plugins\\CLRLoader.dll"
    File "ITMInstaller.dll"
    StrCpy $0 ${NSIS_MAX_STRLEN}
    System::Call 'CLRLoader::Call(w "ITMInstaller::NSIS::ITMInstaller::GetDomain()") w. r0' ? u
    MessageBox MB_OK|MB_ICONINFORMATION  " $0"
    SetPluginUnload manual
    System::Free $0
    Delete "$PLUGINSDIR\\CLRLoader.dll"
FunctionEnd
Section "MainSection" SEC01
  Call loadDll
SectionEnd 
Thanks in Advance for the help.
claesabrandt#
Yes, it is a known issue. I am working on how to fix it, but for now you just have to leave it as is. The file is deleted the next time the user reboots the system. Did you use the new plug-in located at http://nsis.sourceforge.net/Call_.NET_DLL_methods_plug-in, or did you you one of the previous versions?
rbchasetfb#
Thanks for the quick reply. I did get the one from that link. I found that I could delete CLRLoader.dll right after the NSIS installer closed and not have to wait for the reboot.

I actually loaded up the code that was in the Zip and played around a bit in VS2005 (I'm an old c programmer). Your code does a good job of cleaning up and it does successfully free the called .NET assembly as it is deleted on installer exit. It seems its the way that the System::Call is releasing the resources associated with the CLRLoader.dll...or at least CLRLoader.dll is not getting/responding to a command to "unload".

Hope that helps and thanks for coming up with this one. Looking forward to the eventual fix.
LoRd_MuldeR#
Just one question: Is there a specific reason why your "CLR.dll" must be called through the System-Plugin instead of making it a "normal" NSIS plugin? This way the parameters could be passed to the CLR plugin directly, instead of wrapping them into the System plugin parameters. Like this:

CLR::Call 'SomeAssembly.dll::SomeNamespace::SomeClass::SomeMethod("some string value",12,"15.8",false)'
claesabrandt#
There is no reason anymore, and I will soon replace it with a native NSIS plugin.

One reason why it is not a native NSIS plugin yet is that when I startet this, the dll wouldn't be deleted from the temporary folder. But when I called it through the System::Call, the dll was deleted. However, at some point during development of the dll, even when calling through System::Call the dll is not deleted. So at this point it is not anymore a valid argument for not making it a native NSIS plugin.

The more important reason for the delay of the native dll release is that I am not sure if NSIS supports unicode out of the box. I have not yet had any luck passing unicode strings to the native dll. I tried with CHAR and wchar_t in exdll.h, but no luck. Can anyone help me with this?
LoRd_MuldeR#
I don't think NSIS does support Unicode yet. But there is development going into that direction. However I think the strings passed from NSIS to the System-Plugin are *not* Unicode, so passing the None-Unicode strings directly from NSIS to your CLR-Plugin should be no worse at least...

BTW: Do you remember to unload the plugin after all work is done in order to allow the plugin DLL to be delete?

Like this:

CLR::Call /NOUNLOAD 'SomeAssembly.dll::SomeNamespace::SomeClass::SomeMethod("some string value",12,"15.8",false)'
...
(Some other important stuff here)
...
CLR:😁estroy
claesabrandt#
Alright. I have uploaded a new version of the plugin, version 0.2. It is now a native NSIS plugin 🙂 I have updated the wiki page too. Found a bug too and added a parameter type.

Thanks for the feedback rbchasetfb. I have tried in those directions too regarding unloading.

LoRd_MuldeR, regarding making a Destroy function. I am not quite sure what to put in that function.

Another thing. Is there a way to get to know how many parameters I have passed in, so I can skip passing in the int with number of parameters?

Wiki page: http://nsis.sourceforge.net/Call_.NET_DLL_methods_plug-in.
LoRd_MuldeR#
Originally posted by claesabrandt
LoRd_MuldeR, regarding making a Destroy function. I am not quite sure what to put in that function.[/B]
The important thing is, that you call your Plugin one time *without* the "/NOUNLOAD" option, before the installer exists. Otherwise the DLL won't be freed and thus cannot be deleted. If you don't have any resources to free inside your DLL, simply implement a dummy "Destroy" method. Sidenote: Calling the plugin with /NOUNLOAD will prevent the DLL from loosing it's "internal" state after every single call. It will also prevent the DLL from being re-initialized at/before very single call...

Originally posted by claesabrandt Another thing. Is there a way to get to know how many parameters I have passed in, so I can skip passing in the int with number of parameters?[/B]
Nope, you cannot. NSIS arguments are simply passed on the NSIS stack and your plugin MUST know how many arguments it has to pop off the stack. I highly recommend you don't let the user call your plugin directly! Provide some macros instead. This makes it much safer and much more easy to use as well!

!macro _CallCRL dll namespace class method args
CLR::Call /NOUNLOAD `${dll}::${namespace}::${class}::${method}(${args})`
!macroend

!define CallCRL "!insertmacro _CallCRL"
Section
${CallCRL} "SomeAssembly.dll" "SomeNamespace" "SomeClass" "SomeMethod" '"some string value",12,"15.8",false'
SectionEnd
rbchasetfb#
Thanks for the speedy update, claesabrandt. I've downloaded and tried your example, but when I call pop $0 after CLR:Call, $0 contains the name of my DLL. If I call pop $0 again, it contains the "Namespace.Classname" parameter. I'm probably missing something, but I did use the testscript.nsi you included, just don't seem to be able to get the return string in $0.
LoRd_MuldeR#
Returning a value could also be "optimized" with a macro:

!macro _CallCRL return_var dll namespace class method args
CLR::Call /NOUNLOAD `${dll}::${namespace}::${class}::${method}(${args})`
Pop ${return_var}
!macroend

!define CallCRL "!insertmacro _CallCRL"
Section
${CallCRL} $0 "SomeAssembly.dll" "SomeNamespace" "SomeClass" "SomeMethod" '"some string value",12,"15.8",false'
MessageBox MB_OK "Return value is: $0"
SectionEnd
claesabrandt#
rbchasetfb, I had the same problem, but then I found out that I still was compiling the old CLR.dll with the installer. Make sure no CLR.dll is located in the script's directory and that you do not call File CLR.dll. Just put the CLR.dll in NSIS's plugins dir. Let me know if it helps.
claesabrandt#
Just corrected a bug where if more that one class were found in the .net assembly, only the first class could be loaded. Version 0.3 can be downloaded now and the wiki has been updated.

Wiki: http://nsis.sourceforge.net/Call_.NET_DLL_methods_plug-in