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.
Calling managed .NET DLL from NSIS - this works :-)
112 posts
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:
Here's the itreplace function:
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.
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?
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.
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.
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.
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.
Hmm, I now even tried compiling a C++ Win32 DLL without any .NET. This is only code from exdll.c and exdll.h. It does not run on the machine that my .NET plugin could not run on. Anyony any ideas?
I finally found the problem! My CLR.dll must be compiled in release mode and the system onto which the installer is going to run must have the MS Visual C++ 2008 Redist package installed, which can be included in the installer.
I will release a new version of the zip file (v0.4). Could you test it for me rbchasetfb, so we can verify that the problem is fixed? If you want to use the 2005 edition of the redist, you can just compile the CLR.dll with VS2005 instead.
Would there be a greater wish for having it use the 2005 redist as default instead?
I will release a new version of the zip file (v0.4). Could you test it for me rbchasetfb, so we can verify that the problem is fixed? If you want to use the 2005 edition of the redist, you can just compile the CLR.dll with VS2005 instead.
Would there be a greater wish for having it use the 2005 redist as default instead?
Just uploaded version 0.5 which is compiled with Visual Studio 2005, release mode. The need for MS Visual C++ 2008 Redist is now eliminated, in fact, it seems the 2005 redist is not needed either - just the .NET 2.0 framework. Thanks goes to rbchasetfb for great input.
claesabrandt, very cool that we got through that one. Thanks for sticking with it too. As a thanks, I thought I would post some code that I use in my .NET dll that CLR.dll is calling. It allows the functions in the .NET dll to update the Details list during the install without have to wait to return to the installer and then do a DetailPrint call with some funky string work. The code is in VB.NET, but could easily be converted to C#. The error handling is a little brute force in the MakeLogEntry function, it just ignores the error and returns, potentially writing nothing to the Details list. Non-the-less, very useful I think.
VB.NET Code Required:
VB.NET Code Required:
Imports System
Imports System.Runtime.InteropServices
Imports System.Threading
Imports System.IO
Imports System.Windows.Forms
Namespace NSIS
Public Class TestClass
//LVITEM Structure Declaration.
<StructLayout(LayoutKind.Sequential)> _
Public Structure LVITEM
Public mask As Int32
Public iItem As Int32
Public iSubItem As Int32
Public state As Int32
Public stateMask As Int32
Public pszText As String
Public cchTextMax As Int32
Public iImage As Int32
Public lParam As IntPtr
End Structure
//SendMessage API declarations.
Public Declare Function SendMessageLV Lib "user32" Alias _
"SendMessageA"(ByVal hwnd As IntPtr, _
ByVal wMsg As Int32, _
ByVal wParam As Integer, _
ByRef lParam As LVITEM) As Integer
Public Declare Function SendMessage Lib "user32" Alias _
"SendMessageA"(ByVal hwnd As IntPtr, _
ByVal wMsg As Int32, _
ByVal wParam As Integer, _
ByRef lParam As Integer) As Integer
//Constants used by LVITEM and SendMessage
Public Const LVM_FIRST as Integer = &H1000
Public Const LVM_GETITEMCOUNT as Integer = LVM_FIRST + 4
Public Const LVM_GETITEM as Integer = LVM_FIRST + 5
Public Const LVM_INSERTITEM as Integer = LVM_FIRST + 7
Public Const LVM_SCROLL as Integer = LVM_FIRST + 20
Public Const LVIF_TEXT as Integer = &H0001
Public Const LVIS_FOCUSED as Integer = &H0001
//Test function for calling from an NSIS installer using CLR.dll
Public Function ChopString( _
ByVal hwnd as IntPtr, _
ByVal stringToChop as String, _
ByVal chopToLength as Integer) As String
Dim ret as String = ""
If stringToChop.Length > chopToLength Then
MakeLogEntry(hwnd, "Chopping String '" & stringToChop & "'.")
ret = stringToChop.SubString(0,chopToLength)
MakeLogEntry(hwnd, "String chopped to '" & ret & "'.")
Else
MakeLogEntry(hwnd, "String '" & _
stringToChop & _
"' to short to chop to length '" & _
chopToLength & "'.")
End If
Return ret
End Function
//Here's the key function that writes to the Details list.
Public Sub MakeLogEntry(ByVal hwnd As IntPtr, entry as String)
Try
//Get current list count
Dim c as Integer = SendMessage(hwnd,LVM_GETITEMCOUNT,0,0)+1
//Setup a LVITEM structure for the Insert message
Dim lv As New LVITEM
lv.iItem = c
lv.pszText = entry
lv.mask = LVIF_TEXT
lv.stateMask = LVIS_FOCUSED
lv.state = LVIS_FOCUSED
//Insert the LVITEM into the list
c = SendMessageLV(hwnd, LVM_INSERTITEM, 0, lv)
//Scroll the list so the item is visible
SendMessage(hwnd,LVM_SCROLL,0,12)
Catch ex As Exception
'Do Nothing, just return
End Try
End Sub
End Class
End NameSpace NSIS Code with CLR.dll call to test with:Name "Test CLRDLL MakeLogEntry"
OutFile "TestCLRDLL.exe"
ShowInstDetails show
Var DetailsHWND
Page instfiles
Section
DetailPrint "Starting Test"
InitPluginsDir
SetOutPath $PLUGINSDIR
File "TestCLRDLL.dll"
FindWindow $0 "#32770" "" $HWNDPARENT
GetDlgItem $DetailsHWND $0 1016
CLR::Call "TestCLRDLL.dll" \
"NSIS.TestClass" \
"ChopString" \
3 \
$DetailsHWND "Testing Make Log Entry" 9
Pop $0
DetailPrint "ChopString Result = $0"
DetailPrint "Test Complete"
SectionEnd Lastly, I've attached a zip file of the TestCLRDLL.dll project for VS2005 and included the TESTCLRDLL.nsi as will. Enjoy. I hope it's as useful for use as it is for me.Weeeeeee I cannot say how enthusiastic I am that you post this sample. I really need this too. Cool stuff. Thanks so much 🙂
You should make a nsis wiki sample page with this. Would be very beneficial.
You should make a nsis wiki sample page with this. Would be very beneficial.
🙂 Glad you needed it...thought it would be handy. Will work on the wiki page over the next couple of days.
You can use my old wiki page on the subject. The code in it is not valid anymore, and the explanation on how to make the CLR.dll is outdated and users should look in the source code for the CLR.dll plugin instead. It is located here: http://nsis.sourceforge.net/Calling_Managed_.Net_DLL_From_NSIS
You can just change the author and move the page to another name, that fits with your purpose. If you want I can make the initial posting for you. Or you can make a brand new page if you want to.
You can just change the author and move the page to another name, that fits with your purpose. If you want I can make the initial posting for you. Or you can make a brand new page if you want to.
If anyone needs to convert that VB.NET code to C#, here is a useful link: http://labs.developerfusion.co.uk/convert/vb-to-csharp.aspx. When converted some minor ajustments needs to be made. I guess I could post the C# version when I get home tonight.
Btw rbchasetfb, I noticed in your sample that you call CLR::Call without /NOUNLOAD. I found that if you attempt to call CLR:Call again after that, the installer hangs. You must specify the /NOUNLOAD each time. Then when done, call CLR:: Destroy, for instance in .onGUIEnd (without the space but smileys are attacking me). For instance:
Btw rbchasetfb, I noticed in your sample that you call CLR::Call without /NOUNLOAD. I found that if you attempt to call CLR:Call again after that, the installer hangs. You must specify the /NOUNLOAD each time. Then when done, call CLR:: Destroy, for instance in .onGUIEnd (without the space but smileys are attacking me). For instance:
CLR::Call /NOUNLOAD parameters...
CLR:: Destroy I will update the plugin wiki with this information.Thanks, claesabrandt, I'll get that page setup to on the wiki. I've also made the changes to the example. It is the way I'm using CLR.dll, just forgot to put it in the example. I've uploaded the new zip here until I get the wiki page done.
Wiki Page For DetailPrint from Inside .NET DLL
The wiki page for DetailPrint from Inside .NET DLL is finally up. I've posted some modified source code and some other things of value along with it, including the complete VS2005 project for the TestCLRDLL.
http://nsis.sourceforge.net/DetailPrint_From_Inside_.NET_DLL
claesabrandt, maybe you can put a link to it on your NSIS wiki page as I've posted it to the Code Examples category since it's not really a plugin.
Enjoy.
The wiki page for DetailPrint from Inside .NET DLL is finally up. I've posted some modified source code and some other things of value along with it, including the complete VS2005 project for the TestCLRDLL.
http://nsis.sourceforge.net/DetailPrint_From_Inside_.NET_DLL
claesabrandt, maybe you can put a link to it on your NSIS wiki page as I've posted it to the Code Examples category since it's not really a plugin.
Enjoy.
Really nice article. I made a link to it from the plugin page.
Clasesabrandt, thanks for putting this plugin together. It's turning into a life saver for me as I migrate from the Installer projects in VS 2008.
I've got my install actions from my VS 2008 installer ported over to use your plugin, but I'm stuck trying to figure out how to run a .NET method during uninstall. It seems like the DLL file used during installation is not available. Any thoughts or suggestions?
I've got my install actions from my VS 2008 installer ported over to use your plugin, but I'm stuck trying to figure out how to run a .NET method during uninstall. It seems like the DLL file used during installation is not available. Any thoughts or suggestions?
If you need to perform some .NET call on uninstallation, I would keep a copy of the dll file in $INSTDIR, then when uninstalling I would copy that dll file to $PLUGINSDIR.
Improved Tokenizer
claesabrandt,
I found a solution to the need for passing the parameter count. When we talked before, I had implemented a parameter parser that didn't take into account commas in string parameters, well, I found the fix for that. Found a string tokenizer code module that allows you to spec the delimiters, constraining quotes that will cause delimiter to be ignored and a couple other cool options. To use this, replace the following code in NSIS_CLR_Loader.cpp:
Lastly, the only catch I found is that when passing file paths, they need to have double backslashes. I'm sure there's a fix in the c++ code somewhere, just haven't found it yet. So I just modified my NSIS macro that calls CLR::Call to do a string replace of single backslashes with double backslashes.
Here's the CodeProject website that I found this tokenizer at.
http://www.codeproject.com/KB/stl/tokenizer.aspx
Enjoy.
claesabrandt,
I found a solution to the need for passing the parameter count. When we talked before, I had implemented a parameter parser that didn't take into account commas in string parameters, well, I found the fix for that. Found a string tokenizer code module that allows you to spec the delimiters, constraining quotes that will cause delimiter to be ignored and a couple other cool options. To use this, replace the following code in NSIS_CLR_Loader.cpp:
// num params
popstring(buf);
int numparams = atoi(buf);
// params
vector<string> args;
for (int i=0; i<numparams; i++)
{
popstring(buf);
string tmp = string(buf);
args.push_back(tmp);
} With the following code: // params
popstring(buf);
string sargs = string(buf);
vector<string> args;
tokenize (sargs,args,",","","\\\\'","\\\"); Add the following include directive:#include "tokenizer.h" Then add the code and header files from the attached zip file. Lastly, the only catch I found is that when passing file paths, they need to have double backslashes. I'm sure there's a fix in the c++ code somewhere, just haven't found it yet. So I just modified my NSIS macro that calls CLR::Call to do a string replace of single backslashes with double backslashes.
Here's the CodeProject website that I found this tokenizer at.
http://www.codeproject.com/KB/stl/tokenizer.aspx
Enjoy.
Very nice code you found there. However I prefer the current method as it keeps the parameter list cleaner and easier to read. Having them inside a single string is harder to read.
What do you think? Is it better to have all parameters in one string, or is it better to have them seperated?
Actually, I could just make an additional function with the tokenizer, so people can choose themselves 🙂 Should I do that?
What do you think? Is it better to have all parameters in one string, or is it better to have them seperated?
Actually, I could just make an additional function with the tokenizer, so people can choose themselves 🙂 Should I do that?
I'm trying to use this plugin for the first time and get an error message:
Error calling .NET DLL method
Number of parameters does not match
I'm trying to call my C# method:
Thanks!
Error calling .NET DLL method
Number of parameters does not match
I'm trying to call my C# method:
public static bool LogTest(string eventSource) with a line of code like this:CLR::Call /NOUNLOAD Util.dll Util LogTest "Sample" and also tried this variant (unsure if I need to specify the number of parameters):CLR::Call /NOUNLOAD Util.dll Util LogTest "Sample" 1 What am I doing wrong?Thanks!
You have to put the 1 before the parameter and it seems you are missing the namespace, should be something like this:
CLR::Call /NOUNLOAD Util.dll SomeNamespace.Util LogTest 1 "Sample" Thanks, putting the number of parameters first did the trick :-)
It might be worth updating the Wiki a bit to make the parameter passing scheme clearer. Reading through this thread, there have been a few iterations on how to pass parameters so something in the Wiki like:
CLR::Call /NOUNLOAD MyModule.dll My.Name.Space MyMethod [NumberOfParameters] [Parameter List]
Where [NumberOfParameters] is the integer number of parameters present
[Parameter List] is a list of string, float or integer parameters separated by space. String parameters must be enclosed in quotes.
Would make things a bit clearer to those new to NSIS and this plugin.
It might be worth updating the Wiki a bit to make the parameter passing scheme clearer. Reading through this thread, there have been a few iterations on how to pass parameters so something in the Wiki like:
CLR::Call /NOUNLOAD MyModule.dll My.Name.Space MyMethod [NumberOfParameters] [Parameter List]
Where [NumberOfParameters] is the integer number of parameters present
[Parameter List] is a list of string, float or integer parameters separated by space. String parameters must be enclosed in quotes.
Would make things a bit clearer to those new to NSIS and this plugin.
Did you read the wiki here: http://nsis.sourceforge.net/Call_.NET_DLL_methods_plug-in.
Could not load ...\CLR.dll
I am running into a situation where I am unable to load the CLR.dll on 3 out of 5 machines. I have checked the .Net Framework versions and have concluded that although there are some discrepancies, at least two of the failing machines match the Framework version of one of the machine which does work.
Is there an enhanced logging mechanism I can invoke to obtain more information relating to the inability to load the library?
When it works, I am in love with plugin. Thank you VERY much!
- SlyW
P.S. If you want my .nsi and .Net DLL, please ask.
I am running into a situation where I am unable to load the CLR.dll on 3 out of 5 machines. I have checked the .Net Framework versions and have concluded that although there are some discrepancies, at least two of the failing machines match the Framework version of one of the machine which does work.
Is there an enhanced logging mechanism I can invoke to obtain more information relating to the inability to load the library?
When it works, I am in love with plugin. Thank you VERY much!
- SlyW
P.S. If you want my .nsi and .Net DLL, please ask.