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


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

Nice! Please could you make a Wiki page for this?

Stu


Done. It is now located here:
http://nsis.sourceforge.net/Calling_Managed_.Net_DLL_From_NSIS


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.


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.


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


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


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.

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.

You can use `` for outer quotes saving you having to use $\" for ".

Stu


Excellent, thanks :). I corrected the post with your suggestion.


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.


Sure keep as both and let the user decide which to use.

Stu


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.


Sorry I just deleted the wrong one. Either way it would be best to upload to the Wiki instead.

Upload to for example:
http://nsis.sf.net/File:CLRLoader.zip

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

Stu


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.


I noticed your float has a comma in it rather than a full stop in the sample code.

Stu


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.


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.

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?


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.


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

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?


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::Destroy

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.


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

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.


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

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.


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


Ok. Here's the latest.
TEST SCENARIO 1:
Downloaded your new v0.3. Copied the CLR.dll to my NSIS\Plugins folder and insured that CLR.dll exists no where else. Compiled and ran the test script and it returned the dll file name again.

TEST SCENARIO 2:
So, took the code from the zip file, converted the vcproj file to work in VS2005 (changed version to 8.00 and removed the MinFrameworkVersion attribute from the AssemblyReference tags), opened the vcproj in VS2005 and compiled it. Insured the new CLR.dll was in the plugins folder and built and ran the test script...got the right result back from the .NET function called!!! However, the DLL was left behind in $PLUGINSDIR again.

TEST SCENARIO 3:
So, opened up a virtual machine of mine and installed VS C++ 2008 Express, loaded the unaltered code from the zip file, compiled it. Moved it to NSIS\Plugins, built and ran the test script and got the same result as in Test Scenario 1 above, the dll filename was returned.

BTW Thanks for working on this one as much as you are. It has some great value. I will use the one I built in Scenario 2 for now, but look forward to any advancements you make.


That is really strange. I'm already compiling it for .NET framework 2.0, just using VS2008 to do it. I cannot reproduce the error at all :( I wil continue to try to reproduce the error in various ways.


claesabrandt, I think I fixed the need for passing the number of parameters. In the Call function in your code, replace the num params and params sections with the following code and add the itreplace function above the Call function in the code.
Here's the code to replace in the Call function:


    // params

popstring(buf);
string sargs = string(buf);
vector<string> args;
string separator = string(",");
std::string::size_type start = 0;
std::string::size_type end = 0;
while ((
end=sargs.find (separator, start)) != std::string::npos)
{
string tmp = sargs.substr(start, end-start);
itreplace(tmp,"'","");
args.push_back(tmp);
start = end+separator.size();
}
string tmp = sargs.substr (start);
itreplace(tmp,"'","");
args.push_back(tmp);
Basically, this code performs a split function on the string popped of the stack and adds each split-off element to the args vector, performing a replace of the single quotes along the way.

Here's the itreplace function:
void itreplace(string &source, const string find, string replace) 

{
size_t j;
for ( ; (j = source.find( find )) != string::npos ; )
{
source.replace( j, find.length(), replace );
}
}
Then, in the NSIS project, do the plugin call this way:
CLR::Call "Somedotnet.dll" "namespace.class" "TestFunction" "Testing String, 35.56, true" 
OR...the code has the provision to do it this way with single quotes around strings to be visually clean:
CLR::Call "Somedotnet.dll" "namespace.class" "TestFunction" "'Testing String', 35.56, true" 
You can also leave the spaces out after the commas in the parameter list if you like.

On the issue of the strange results from my test scenario's I wrote about earlier, I've even tried the tests on other machines and get the same results. My last test runthrough was on a Windows 2003 Server with only .net 2.0 installed. For now, I'll live with the stay-behind dll file and use the VS2005 compiled one with the changes above.

Thanks again. Let me know if you find any bugs in the above code.

Thanks for the reply, it is nice to have other eyes on this, I appreciate your help :)

I had that model ealier too (splitting a parameters string), but what happens if your strings has commas in them? Actually, I still prefer having the parameters as they are now in the plugin, as it is cleaner to read instead of having them all in one string. I do have some code to split that string taking into acount that strings can have commas in them that are not two seperate parameters. But, isn't it okay as it is now?

What I would like to spend some time on is 1) Getting the DLL to be deleted after install and 2) Investigating the problem you encountered with the return value. Could it be the stack that is corrupted in some way?


Okay, maybe on to something here. That return value being the dll name problem seems to stem from the version of .NET installed.

My development box, and the two machines I was testing on, both had .NET 2.0 installed as well as the .NET 2.0 Service Pack 1 and .NET 3.0 Service Pack 1.

When I went to a machine with no .NET 2.0 installed, then installed the base .NET 2.0 redistributable with no Services packs, the dll name showed up as the return value.

Once I installed just the .NET 2.0 Service pack 1, the problem resolved and I got expected responses from the dll calls.

BTW - this is all with my VS2005 compiled DLL. I still get the dll name returned with your VS2008 compiled DLL on the new .NET 2.0 w/SP1 machine.

This may be coming down to some variations of the .NET 2.0 environment based on other .NET Framework installs. If I'm not mistaken, VS2008 requires .NET 3.5 installed. Maybe that has affected the .NET 2.0 assemblies in some way.


claesabrandt, can you check the file version of you System.dll in the C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727 folder. Mine reads 2.0.50727.1433. It seems that the .1433 build number is the important part, when its .42, my VS2005 build doesn't work (dll calls return the dll filename).

I've tried installing .NET 3.5 SP1 on my test machine which installs .NET 2.0 System.dll with a file version of 2.0.50727.3053 and your compiled CLR.dll returns the dll filename as before.


Mine reads 2.0.50727.1433 as yours. Now that you mention this, I have a vmware machine where my plugin sometimes returns a bogus value. I will investigate too, but it sounds like the stack is getting messed up in some way.


I was thinking it might read something differently entirely on your machine than .1433, .42 or .3053. So it would seem that that is not it. I'm at a loss on this issue as neither of us are able to both compile the same CLR.dll and subsequently recreate the problem.

At this point, I've changed my prerequisite NSIS code to install NetFx20SP1_x86.exe instead of the usual dotnetfx.exe. This insures .NET 2.0 SP1 is installed. Additionally, I'm using the VS2005 compiled version of the CLR.dll.


Hmm, I just tried it on my vwmare machine, and on that System.dll has the same version 2.0.50727.1433. Here the call actually fails and has the same result as you experienced - it returns the name of the dll file. Don't worry, we will eventually find the problem :)


Wait, it seems the problem is completely different. NSIS actually outputs the error "Could not load "..blabla..CLR.dll". However, the DLL has been copied by NSIS to the correct location, so it is some load error.