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

1

25

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.

Nonpareil answered 29/9, 2015 at 10:39 Comment(11)
Erm, so IntelliSense show it as false and the compiler interprets it as false, it is only the Resharper decompiler that shows true. How is that not a Resharper bug? Looks like a COM component, refresh the interop library by running the .NET 4 version of Tlbimp.exeHae
I believe the correct default value should be true because that means write to the buffer and flush it immediately. And yes it's a COM component. I will try your suggesstion asap.Nonpareil
@HansPassant Can you show how to use the Tlbimp.exe tool? Thanks!Nonpareil
Hard to see the point. Paste "how to use the Tlbimp.exe tool" into the Google query box. Top two hits are very good, if you still have a question about it then click the Ask Question button.Hae
All I found was the msdn page that explains what the tool does. It seems it does lot of interesting things and has so many command line arguments that I didn't bother to read. So if you can provide me with the right args that will do what you wanted it'd be more than welcomed. Also, I've updated my post with extra info that might help readers.Nonpareil
I believe the problem may be related to the NET 3.5 referenced by your system. Some comments around are pointing some bugs when utilizing VS2015 and NET 3.5 Assembly - even indirect usage. Can you change these controls to NET 4.0 (or above) version?Spavined
@David BS Unfortunately I have no control over the 3.5 assembly I'm referencing. It's a third party library.Nonpareil
I think this is the wrong place for bug reports. Go to Connect, and post your problem over there. They might be able to reproduce and fix it.Harriman
@PatrickHofman I don't know, I think it's a good place to get non-bugs shot down by interested amateurs before bothering the professionals with them.Issus
@PatrickHofman This is not really a bug report (I now changed the title to a straightforward question), I'm just trying to get help with understanding why this wrong behavior is happening and I think Stackoverflow is the right place for this kind of problems.Nonpareil
Is it possible that interface and class implementation have different default values?Eichmann
H
26

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.

Hae answered 4/10, 2015 at 21:46 Comment(3)
Thanks for taking the time and effort of analyzing and reproducing the bug. Unfortunately the library comes from a vendor so I can't recreate it myself. I will have to contact them. I have reported the bug and linked to this thread.Nonpareil
Digging into some of the articles, it looks like some of the issues only occur during Release compiles with the optimizer on. Microsoft released several official CLR patches November 2015. Hopefully this specific issue was also corrected.Dandle
Checking it again in VS2015 Update 2, it now generates a bizarre error message. "error CS1729: 'SWbemQualifierSetClass' does not contain a constructor that takes 0 arguments". Well, it doesn't generate bad code anymore. Progress.Hae

© 2022 - 2024 — McMap. All rights reserved.