Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do optional parameters get passed wrong values in Visual Studio 2015?

Tags:

I found a weird behavior in VS2015 Here are the details:

I have a .Net 4.6 project referencing a 3.5 assembly. This assembly defines in one of it's interfaces the following method that I was able to inspect using Resharper decompiler.

void WriteString([MarshalAs(UnmanagedType.BStr), In] string data, [In] bool flushAndEND = true); 

Take note of the last optional argument flushAndEND which has a default value of true. The problem now is when I use this method in my project, hovering over the method name shows the usual VS toolTip which details the method signature, except that for me it shows a wrong default value of the optional argument flushAndEND. Here's a screenshot

enter image description here

To make things even worse, I have noticed that during runtime, when calling the method WriteStringwith only the first parameter, flushAndEND gets set to false and not its default value defined in the DLL I'm referencing. The impact of this on our project was big because it rendered a big feature of our app useless and blocked a big part of our regression tests.

I was able to overcome this problem by forcing the value of the optional argument to true when calling the method, but I'm afraid there are other calls somewhere else in the project that suffer from the same problem. So I will need a better solution for this or to at least understand what's the cause behind this behavior.

We've just upgraded our environment weeks ago. Before we were using VS2013 and everything worked fine.

I am aware of the confirmed .Net 4.6 bug which causes some arguments to be passed wrong values and I can relate it to my issue here, but as it is said in the article, the bug only occurs when compiling for x64 architecture. My project is a WPF application and we compile it to be x32.

Why is WriteString called with wrong default argument?

I will try later to isolate the problem in a small project and see if I can reproduce the problem.

EDIT: I've managed to isolate the problem, and found some interesting stuff!

I created a simple .Net 4.6 console application, added a reference to my Dll and wrote the following simple code that consist of sending a command to a device and reading the response:

private static void Main(string[] args)     {          //Init managers         ResourceManager ioMgr = new ResourceManagerClass();         FormattedIO488 instrument = new FormattedIO488Class();          //Connect to the USB device         instrument.IO = (IMessage)ioMgr.Open("USB0::0x0957::0x0909::MY46312358::0::INSTR");           string cmd = "*IDN?";          //This is the problematic method from my dll         instrument.WriteString(cmd);          //Read the response         string responseString = instrument.ReadString();         Console.WriteLine(responseString);         Console.ReadKey();     } 

What I did next, is open this project from both VS 2013 and VS 2015. In both versions of VS I did rebuild the project and run it. Here are the results:

VS2013: WriteString was called using the CORRECT default value of flushAndEND (which is true meaning flush the buffer and end the command).

VS2015: WriteString was called using the WRONG default value of flushAndEND which gave a timeout exception.

Further inspections between the two versions of Visual Studio shows that the object browser viewer in VS2013 shows the method signature as:

void WriteString(string data, [bool flushAndEND = True]) 

while the object browser in VS2015 shows the method signature as:

void WriteString(string data, [bool flushAndEND = False]) 

The only explanation of this behavior is that there's a problem with VS2015 compiler not reading correct default values from the assembly.

like image 950
disklosr Avatar asked Sep 29 '15 10:09

disklosr


People also ask

Why are optional parameters bad?

The thing with optional parameters is, they are BAD because they are unintuitive - meaning they do NOT behave the way you would expect it. Here's why: They break ABI compatibility ! so you can change the default-arguments at one place.

How do you pass optional parameters?

By Params Keyword: You can implement optional parameters by using the params keyword. It allows you to pass any variable number of parameters to a method. But you can use the params keyword for only one parameter and that parameter is the last parameter of the method.

Is adding an optional parameter a breaking change?

Common examples of additive, non-breaking changes include: Adding a resource or method. Adding a response field. Adding optional query parameters.

Why are optional parameter added?

Optional Parameters are parameters that can be specified, but are not required. This allows for functions that are more customizable, without requiring parameters that many users will not need.


1 Answers

Okay, I found a way to reproduce this bug that anybody can see for themselves. And above all, the Microsoft programmers that work on Roslyn that need to fix this. There was enough of a lead in the question that this is an issue that is specific to COM interop libraries. That panned out.

I searched for a type library that's available widely with a method that has a bool argument with a default of true. There is exactly one, what are the odds :) It is SWbemQualifierSet.Add() method, it takes 3 boolean arguments that all have a default of true.

I first generated the interop library by running this command from the Visual Studio Command Prompt:

   tlbimp C:\Windows\SysWOW64\wbem\wbemdisp.tlb 

Which produces a WbemScripting.dll interop library. Then wrote a little test app that calls the method, adding the WbemScripting.dll interop library as a reference:

class Program {     static void Main(string[] args) {         var obj = new WbemScripting.SWbemQualifierSet();         object val = null;         obj.Add("foo", ref val);     } } 

Beware that it doesn't actually run, we're only interested in the code it generates. Looking at the assembly with ildasm.exe:

  IL_001e:  ldstr      "foo"   IL_0023:  ldloca.s   val   IL_0025:  ldc.i4.1   IL_0026:  ldc.i4.1   IL_0027:  ldc.i4.1   IL_0028:  ldc.i4.0   IL_0029:  callvirt   instance class WbemScripting.SWbemQualifier WbemScripting.ISWbemQualifierSet::Add(string,                                                                                                          object&,                                                                                                          bool,                                                                                                          bool,                                                                                                          bool,                                                                                                          int32) 

No problems, the ldc.i4.1 opcodes pass true. Both Object Browser and IntelliSense properly show true as the default.


Then I ran the oldest version of Tlbimp.exe that I could find on my machine. It generates a .NET 2.0.50727 compatible assembly:

  "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\TlbImp.exe" c:\windows\syswow64\wbem\wbemdisp.tlb 

Rebuild the test project, this time it looks like this:

  IL_001e:  ldstr      "foo"   IL_0023:  ldloca.s   val   IL_0025:  ldc.i4.0   IL_0026:  ldc.i4.0   IL_0027:  ldc.i4.0   IL_0028:  ldc.i4.0   IL_0029:  callvirt   instance class WbemScripting.SWbemQualifier WbemScripting.ISWbemQualifierSet::Add(string,                                                                                                          object&,                                                                                                          bool,                                                                                                          bool,                                                                                                          bool,                                                                                                          int32) 

Problem reproduced, note how ldc.i4.0 now passes false. Your exact scenario. Everything else behaves like it should, both Object Browser and IntelliSense show false as they should. It just doesn't match the default value that's specified in the COM type library.


Every other version of Tlbimp.exe I have available, SDK version 7.1 and up generate good code. They all generate .NET v4.0 assemblies.

Characterizing the bug is not that easy. I don't see an obvious flaw when I decompile the "bad" interop library, it shows the corrects defaults being declared:

.method public hidebysig newslot virtual instance class WbemScripting.SWbemQualifier marshal(interface) Add([in] string marshal(bstr) strName, [in] object& marshal(struct) varVal, [in][opt] bool bPropagatesToSubclass, [in][opt] bool bPropagatesToInstance, [in][opt] bool bIsOverridable, [in][opt] int32 iFlags) runtime managed internalcall {     .custom instance void [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) = { int32(2) }     .param [3] = bool(true)     .param [4] = bool(true)     .param [5] = bool(true)     .param [6] = int32(0)     .override WbemScripting.ISWbemQualifierSet::Add } 

So it is unsurprising that Resharper disagrees with Object Browser and IntelliSense, it surely disassembles by itself and doesn't rely on .NET metadata interfaces so shows true as the default.

I must therefore assume that Roslyn is sensitive to the target runtime version. In other words, this will only go wrong with old COM interop libraries that were created with tooling older than .NET 4.0. Otherwise not wildly strange, C# did not start supporting default arguments until v4 and there were incompatible ways to specify the default value. Worst-case scenario is having to use a PIA that's supplied by a vendor. Mitigating circumstance is that default values other than 0/false/null are not that common. Simplest way to see a problematic library is by looking at the assembly with ildasm.exe, double-click the Manifest. Top line:

  // Metadata version: v2.0.50727 

This is certainly breaking behavior for existing projects that are rebuilt with VS2015, please report the bug. Link to this Q+A so you don't have to repeat everything.

The workaround is simple, just re-create the interop library with Tlbimp.exe as I showed. Or remove the interop library and add a reference to the COM component so the interop library is generated on-the-fly when you build. If you depend on a PIA from a vendor then you'll have to ask them for an update or the correct procedure to create a new interop library.

like image 151
Hans Passant Avatar answered Oct 13 '22 11:10

Hans Passant