1

Background: I've got a set of VB6 DLLs that share a common "interface". Whichever version is installed locally has members of this interface invoked via COM interop (from VB.Net code, which I suspect might matter). I noticed today that one of the invocations passes [what I understand to be] an rvalue (hereinafter "an rvalue") to a VB6 function that does not have that particular parameter defined as ByVal.

Example Code:

VB6:

Public Function VB6Function(input As String) As String
    ' Do interesting things with input
End Function

VB.Net:

' get an instance of the VB6 class and pass our trimmed localString to it
result = vb6Instance.VB6Function(localString.Trim())
' Do interesting things with localString

I have not yet noticed an instance of the VB6 code changing the value of input, but I also haven't done an exhaustive search of the different DLL implementations (there are several hundred).

What would happen if VB6Function did change the value of input when input is "an rvalue"? For that matter, why doesn't this method invocation simply error out when "an rvalue" is passed?

Matt Mills
  • 8,692
  • 6
  • 40
  • 64
  • 3
    Thinking of it as an r-value is not useful. System.String is immutable, the return value of Trim() is not extra immutable. The pinvoke marshaller ensures it stays that way, the underlying interop type is BSTR. Created before the call with SysAllocString() and destroyed afterwards with SysFreeString(). The VB6 runtime could reassign the parameter but that just merely causes its string to be destroyed by SysFreeString. The .NET string cannot be affected. – Hans Passant May 18 '20 at 19:08
  • @HansPassant Thank you. Thinking of it as an r-value actually seems to have been the misunderstanding that led me down the rabbit hole. But that's because I conflate "rvalue" with "immutable", so I need to ask a clarifying question to make sure I understand your comment that "the .NET string cannot be affected". If I were to call `VB6Function(localString)` and that code assigned a new value to `input`, would the value of `localString` be different upon `VB6Function` returning? – Matt Mills May 18 '20 at 21:21
  • @arootbeer Of course. That is the main purpose of byref. – GSerg May 19 '20 at 06:46
  • @GSerg I recognize that that is the main purpose of ByRef _within VB6_ and _within VB.Net_. My question was an attempt to clarify whether the behavior is different with a VB.Net COM client calling a VB6 COM server. It was unclear to me whether "Created before the call with `SysAllocString()` and destroyed afterwards with `SysFreeString()`" implies that the reassigned value _would_ still be available after the function call returns, or _would not_ still be available. – Matt Mills May 19 '20 at 13:09
  • Have you tried it? – StayOnTarget May 19 '20 at 14:01
  • I don't understand your question and/or confusion then. The COM function wants a `BSTR*`, regardless of the language it's written or the language that's calling it. So it will be provided with a `BSTR*`. In VB that happens automatically where the compiler arranges a `BSTR*` when you pass a `BSTR`. In e.g. C# it would have not happened automatically and you would need to say `ref localString`. The "Created before the call with SysAllocString() and destroyed afterwards with SysFreeString()" applies to when the compiler knows that *it* arranged the `BSTR*` and therefore needs to destroy it. – GSerg May 19 '20 at 14:32
  • @GSerg - I believe my confusion was based in @HansPassant discussing the immutability of .NET strings in the context of this question. I'm aware that .NET strings are immutable, and while I have no preexisting familiarity with `BSTR` or how native strings work, I did assume that _the marshaller_ would be passing around pointers instead of data structures. So when he brought in the immutability of .NET strings, I wanted to make sure I hadn't missed something more fundamental. – Matt Mills May 19 '20 at 17:48

1 Answers1

4

What would happen if VB6Function did change the value of input when input is "an rvalue"?

Nothing. Or rather, nothing interesting.

When the called function changes the value of its argument, it makes no difference for the insides of that function whether the argument was provided byval or byref. All that matters is that there is a variable of certain type, thus, it can be acted upon.

For that matter, why doesn't this method invocation simply error out when "an rvalue" is passed?

Why would it error out? The passed argument as correct type (string), that is all that matters.

There is no notion of an rvalue in VB.
When you pass what you would call an rvalue to a method accepting something by reference, the compiler automatically passes the reference to a temporary location where the rvalue actually resides. The method gets its value byref, the caller does not care about pointers.

localString.Trim() allocates and returns a string. It has an address and can be passed around. Your code does not explicitly capture that address, but the compiler has no problem passing it to VB6Function byref. If VB6Function changes the value, it changes what that temporary location points to, which has no observable difference because it's going to be destroyed after the call either way.

As for why some people may have preferred receiving strings byref in VBA, it's specifically to avoid copying the entire string each time when calling the function. In VB.NET it's not a problem because strings there are immutable and therefore can be passed byval without copying, but in VBA that is not the case, so a byval string needs to be cloned for the purpose of the call. People avoided that by specifying byref, even though that technically gave them the power to mess with the passed variable.

GSerg
  • 76,472
  • 17
  • 159
  • 346
  • Interesting analogue between this behavior and the behavior in [your linked answer](https://stackoverflow.com/a/8070104/182654): "Because you are using parentheses, the value is forcibly passed by value, not by reference (the compiler creates a temporary copy and passes _that_ by reference)." I recognize that that particular _calling convention_ (calling subs with parens) changed between VB6 and VB.Net, but the analogue is very instructive because it demonstrates the same _behavior_ even though the invocation mechanism (VB6->VB6 vs VB.Net->VB6) is different. – Matt Mills May 18 '20 at 17:43
  • @arootbeer Using parens as in the linked answer is 99% a mistake w/ 1% left for those cases of bad coding style. If a temp copy is needed something like `CLng(longVar)` or `CStr(stringVar)` is used to make the idea explicit. – wqw May 18 '20 at 20:42
  • @wqw If I saw `CLng(longVar)`, where `longVar` is actually `Long`, it would certainly not make the idea explicit to me. Instead I would think it's leftover code from times where `longVar` was not `Long`, and I would remove the `CLng`. [Parentheses](https://stackoverflow.com/a/10262247/11683), on the other hand, make the intention explicit and unambiguous. – GSerg May 18 '20 at 21:01
  • @GSerg If I saw extra parenthesis on a single parameter function I will surely not start thinking very highly of the original author of this piece of code and will probably try to contact them for a brief chat. Different shops have different conventions. Some use `stringVar & ""` and `longVar + 0`. We always use comments for this -- "pass a copy to prevent param being modified in the following poorly written procedure. note to self: next time put a `ByVal` to protect the innocent" – wqw May 20 '20 at 11:03