How do I pass an array of arguments ByRef with CallByName? - vba

I am currently using CallByName to dynamically call methods. There are several methods which I pick up daily from a table in server along with the arguments. For this reason, I send an array of the arguments to CallByName rather than a param array as I don't know the number of arguments until runtime. Given CallByName expects a paramarray I use a private declare function to bypass the VBA Type definition.
Private Declare PtrSafe Function rtcCallByName Lib "VBE7.DLL" ( _
ByVal Object As Object, _
ByVal ProcName As LongPtr, _
ByVal CallType As VbCallType, _
ByRef Args() As Any, _
Optional ByVal lcid As Long) As Variant
Public Function CallByNameMethod(Object As Object, ProcName As String, ByRef Args () As Variant)
AssignResult CallByNameMethod, rtcCallByName(Object, StrPtr(ProcName), VbMethod, Args)
End Function
Private Sub AssignResult(target, Result)
If VBA.IsObject(Result) Then Set target = Result Else target = Result
End Sub
This works when I pass an object where the method changes its underlying properties. However, there are some methods where I pass an object and a method which changes the values of the passed arguments. For example, I pass an array with the following arguments
Dim Name as String, Value1 as double, Value2 as double, Value3 as double
Dim Array(3) as Variant
String = "Name"
Value1 = 0
Value2 = 0
Value3 = 0
Array(0) = Name
Array(1) = Value1
Array(2) = Value2
Array(3) = Value3
When I Pass that array, the method just returns the array back with the same values, but I am expecting double type values for Array(1), Array(2), Array(3). Any ideas?

The the first clue to the answer lies in the function declaration for rtcCallByName (pulled from the exports table of vbe7.dll):
function CallByName(Object: IDispatch; ProcName: BSTR; CallType: VbCallType; Args: ^SafeArray; out lcid: I4): Variant; stdcall;
Note that Args is declared as a pointer to a SafeArray, but is not declared as an out parameter. This means that the COM contract for the function is basically saying that if you pass a ParamArray the only guarantee that it makes is that it won't change pointer to the ParamArray itself. The ByRef in the Declare Function only indicates that you are passing a pointer.
As for the values inside the ParamArray, there really isn't much Microsoft documentation I can dig up specific to VBA, but the VB.NET version of the documentation gives the second clue:
A ParamArray parameter is always declared using ByVal (Visual Basic).
In the context of CallByName, this make perfect sense. The rtcCallByName function itself doesn't (and can't) know which of the parameters for the called method are themselves declared ByRef or ByVal, so it has to assume that it can't change them.
As far as implementations to work around this limitation, I'd suggest either refactoring to eliminate return values that are passed ByRef in code called by CallByName or wrapping the needed functionality in a class.

Turns out this is actually possible, see the RunMacro method in this post:
https://codereview.stackexchange.com/q/273741/146810
Which copies the paramarray into a variant array to pass to rtcCallByName whilst preserving the ByRef flag of the variants, using this CloneParamArray method:
https://github.com/cristianbuse/VBA-MemoryTools/blob/f01b0818930fb1708caaf5fc99812abdeaa9f1df/src/LibMemory.bas#L890

Related

VBA - Is there any alternative to the array() function to create a empty array in vba?

After August 2019 Windows update there is a problem using the array() function in VBA.
Is there any other way to create an empty array in VBA for the purpose "Using multi value combobox on a form"?
The following statement to clear/delete all the selections:
me.cmbMultivalue=Array()
The array returned by Array() is not just an uninitialized array. It's an initialized array with a lower bound of 0 and an upper bound of -1, thus containing 0 elements. This is distinct from normal, uninitialized arrays, which don't have a lower and upper bound.
You can roll your own array function (which I often do for non-variant arrays).
For a variant array, it's really easy. Just take an input ParamArray, and assign that to a variant array:
Public Function altArray(ParamArray args() As Variant) As Variant()
altArray = args
End Function
Then, you can use altArray() to get your special 0-element array.
However, I'm not sure this is also bugged for that specific version of Access. If it is, we can always create a 0-element array using WinAPI (slightly adapted version of this answer):
Public Type SAFEARRAYBOUND
cElements As Long
lLbound As Long
End Type
Public Type tagVariant
vt As Integer
wReserved1 As Integer
wReserved2 As Integer
wReserved3 As Integer
pSomething As LongPtr
End Type
Public Declare PtrSafe Function SafeArrayCreate Lib "OleAut32.dll" (ByVal vt As Integer, ByVal cDims As Long, ByRef rgsabound As SAFEARRAYBOUND) As LongPtr
Public Declare PtrSafe Sub VariantCopy Lib "OleAut32.dll" (pvargDest As Any, pvargSrc As Any)
Public Declare PtrSafe Sub SafeArrayDestroy Lib "OleAut32.dll" (ByVal psa As LongPtr)
Public Function CreateZeroLengthArray() As Variant
Dim bounds As SAFEARRAYBOUND 'Defaults to lower bound 0, 0 items
Dim NewArrayPointer As LongPtr 'Pointer to hold unmanaged variant array
NewArrayPointer = SafeArrayCreate(vbVariant, 1, bounds)
Dim tagVar As tagVariant 'Unmanaged variant we can manually manipulate
tagVar.vt = vbArray + vbVariant 'Holds a variant array
tagVar.pSomething = NewArrayPointer 'Make variant point to the new variant array
VariantCopy CreateZeroLengthArray, ByVal tagVar 'Copy unmanaged variant to managed return variable
SafeArrayDestroy NewArrayPointer 'Destroy the unmanaged SafeArray, leaving the managed one
End Function
When you declare a new array, it is still an empty array.
i.e. Dim x() As Variant
(1) As you mention in your question, your goal is to clear combobox values by assigning an empty array to it, it seems like this would work:
Dim EmptyArray() As Variant
Me.cmbMultivalue = EmptyArray
(2) Or if that doesn't work, assuming that Me.cmbMultivalue behaves like a regular array, the following would work:
Erase Me.cmbMultivalue
EDIT:
(3) Another possible workaround similar to (1) would be to create a non-empty array and then erase it as such:
Dim x() As Variant
x = Array(1)
Erase x
You could then use x as an empty array.
If all that fails and, as you mentioned, assigning the value Null or vbEmpty didn't work, it seems like your only options would be to revert the problematic Windows update or hope Microsoft can fix this quickly.
Alternative to clear all values from a multi-value field is with SQL. Example:
CurrentDb.Execute "DELETE Table1.Test.Value FROM Table1 WHERE ID = 1"

Class property as ByRef argument not working

I am using a function to modify a series of strings, passing them ByRef as arguments to the modifying function. The caller's string variables are all modified as expected but the one argument which is a class property does not change, should this be possible?
The essentials of the class are:-
Private cRptRef As String
Public Property Get TestRefID() As String
TestRefID = cRptRef
End Property
Public Property Let TestRefID(Test_Ref As String)
cRptRef = Test_Ref
End Property
The function for modifying the strings has the following declaration
Public Function GetTestFileNames(ByRef hdrFile As String, _
ByRef calFile As String, _
ByRef dataFile As String, _
ByRef testRef As String _
) As Boolean
The call to GetTestFilenames is as follows:
If GetTestFileNames(HEADERpath, CALpath, RAWDATApath, _
ref) = False Then
All the string arguments are declared as global strings and are empty ("") before the call. After the call they typicaly have content like "d:{path to file{filename.csv}.
So all these statements in the function populate the target strings OK.
hdrFile = Replace(userFile, "##", PT_Rpt.Info.FindNode(TEST_REF_HDRsuffix).data, , , vbTextCompare)
dataFile = Replace(userFile, "##", PT_Rpt.Info.FindNode(TEST_REF_DATAsuffix).data, , , vbTextCompare)
calFile = Replace(userFile, "##", PT_Rpt.Info.FindNode(TEST_REF_CALsuffix).data, , , vbTextCompare)
But this statement fails to assign anything to its target string
testRef = Mid(userFile, InStrRev(userFile, Application.PathSeparator) + 1)
testRef = Left(testRef, InStrRev(testRef, "_") - 1)
Debug.Print "Class.TestRefID="; testRef
The Debug.Print statement prints the expected string, but the external reference is not affected. Is this something to do with it being a property?
The target string being Class.TestRefID in place of the testRef argument.
If I replace the class property in the argument list with a standard string variable, and then assign that to the class property then it I get the expected result, which seems unnecessary work.
Is there something I'm missing or is this not possible in VBA?
A member access expression is an expression that must first be evaluated by VBA before its result can be passed around.
If I have a Class1 module like this:
Option Explicit
Public Foo As String
And then a quick caller procedure:
Sub test()
With New Class1
bla .Foo
Debug.Print .Foo
End With
End Sub
Sub bla(ByRef bar As String)
bar = "huh"
End Sub
The test procedure will output an empty string.
The reason for this is because when you pass a member to a ByRef parameter of a procedure in VBA, you're not passing a reference to the member - you're passing a reference to the value held by that member.
So the member access expression is evaluated, evaluates to "", so "" is passed ByRef to the procedure, which assigns it to "huh", but the caller isn't holding a reference to the "" value, so it never gets to see the "huh" string that was assigned.
If I replace the class property in the argument list with a standard string variable, and then assign that to the class property then it I get the expected result, which seems unnecessary work
It's not unnecessary work, it's mandated, otherwise nothing is holding a reference to the result of the member expression.
Then again the real problem is a design issue, pointed out by Warcupine: the function doesn't want to byref-return 4 values, it wants to take a reference to this object, and assign its properties.

How to call a vb.net function by name with by ref parameters

We need to call a set of functions (currently around 30 or so) programatically by name. They all have the same signature so I've gone down the "invoke" route. I initially tried using delegates as I recalled doing that previously but that didn't achieve what I was after. So I've now looked at reflection to do it.
Dim webapiType as type = type.gettype("fully.qualified.model.webapi", true, true)
dim webAPIConstructor as system.reflection.constructorinfo = webapitype.getconstructor(type.emptytypes)
dim webapiClassObject as object = webapiConstructor.invoke(new object(){})
dim webapiMethod as system.reflection.methodinfo = webapitype.getmethod("myFunctionName")
dim webapiValue as object = webapimethod.invoke(webapiclassobject, new object(){model, xmlREsponse, msg, exDAL})
That all works fine and calls my function "myFunctionName" but it doesn't set the xmlResponse object value, that stays as nothing. (the function I'm calling has byval model, byref xmlREsponse, byref msg and byref exDal)
If I call the method directly rather than invoking it then the xmlResponse parameter is returned.
So the issue is that the values of the byRef values aren't being returned.
Is there some other way of invoking the method so that the ByRef parameters are returned correctly or do I need to investigate some other concept ?
(I've looked at CalledName and the documentation says that it only passes "ByVal" so that's no use)

how to get a particular data from a function return

I have a method in my class which returns a list of data's
Private Function GetCertificationLevels()
Dim theProgramYearID As Int32 = Convert.ToInt32(lstProgramYear.SelectedValue)
Dim thePositionGroupID As Int32 = Convert.ToInt32(lstPositionGroup.Selected)
Dim theCategoryID As Int32 = Convert.ToInt32(lstCategory.SelectedValue)
filterLevels = _curTree.GetCertificationLevelsList(theProgramYearID, thePositionGroupID, theCategoryID)
Return filterLevels
End Function
I would like to retrieve the _data value only (which are 5261,5263,5262,5264 and 5260)
How to proceed?
If I understand your question right, then you are trying to use the values of the variables theProgramYearID, thePositionGroupID, and theCategoryID in another procedure. I would create an overloaded procedure that contains ByRef arguments, and then use them in the other procedure.
Private Sub CallingProcedure()
'Declare variables to hold the ByRef results.
Dim locYearID, locGroupID, locCategoryID as Integer
'Call the procedure, and pass the variables for processing
Dim FunctionResult = GetCertificationLevels(locYearID, locGroupID, locCategoryID)
'Now do whatever you need with the modified variables
SomeOtherProcedure()
End Sub
Private Overloads Function GetCertificationLevels(ByRef theProgramYearID as Int32, ByRef thePositionGroupID as Int32, ByRef theCategoryID as Int32)
theProgramYearID = Convert.ToInt32(lstProgramYear.SelectedValue)
thePositionGroupID = Convert.ToInt32(lstPositionGroup.Selected)
theCategoryID = Convert.ToInt32(lstCategory.SelectedValue)
filterLevels = _curTree.GetCertificationLevelsList(theProgramYearID, thePositionGroupID, theCategoryID)
Return filterLevels
End Function
The ByRef will ensure that the function is modifying the variables passed to it, and they will have the values assigned to them in the function.
Using LINQ:
Return filterLevels.Select(Function(t) t.Data).ToList()

ByRef vs ByVal Clarification

I'm just starting on a class to handle client connections to a TCP server. Here is the code I've written thus far:
Imports System.Net.Sockets
Imports System.Net
Public Class Client
Private _Socket As Socket
Public Property Socket As Socket
Get
Return _Socket
End Get
Set(ByVal value As Socket)
_Socket = value
End Set
End Property
Public Enum State
RequestHeader ''#Waiting for, or in the process of receiving, the request header
ResponseHeader ''#Sending the response header
Stream ''#Setup is complete, sending regular stream
End Enum
Public Sub New()
End Sub
Public Sub New(ByRef Socket As Socket)
Me._Socket = Socket
End Sub
End Class
So, on my overloaded constructor, I am accepting a reference to an instance of a System.Net.Sockets.Socket, yes?
Now, on my Socket property, when setting the value, it is required to be ByVal. It is my understanding that the instance in memory is copied, and this new instance is passed to value, and my code sets _Socket to reference this instance in memory. Yes?
If this is true, then I can't see why I would want to use properties for anything but native types. I'd imagine there can be quite a performance hit if copying class instances with lots of members. Also, for this code in particular, I'd imagine a copied socket instance wouldn't really work, but I haven't tested it yet.
Anyway, if you could either confirm my understanding, or explain the flaws in my foggy logic, I would greatly appreciate it.
I think you're confusing the concept of references vs. value types and ByVal vs. ByRef. Even though their names are a bit misleading, they are orthogonal issues.
ByVal in VB.NET means that a copy of the provided value will be sent to the function. For value types (Integer, Single, etc.) this will provide a shallow copy of the value. With larger types this can be inefficient. For reference types though (String, class instances) a copy of the reference is passed. Because a copy is passed in mutations to the parameter via = it won't be visible to the calling function.
ByRef in VB.NET means that a reference to the original value will be sent to the function (1). It's almost like the original value is being directly used within the function. Operations like = will affect the original value and be immediately visible in the calling function.
Socket is a reference type (read class) and hence passing it with ByVal is cheap. Even though it does perform a copy it's a copy of the reference, not a copy of the instance.
(1) This is not 100% true though because VB.NET actually supports several kinds of ByRef at the callsite. For more details, see the blog entry The many cases of ByRef
Remember that ByVal still passes references. The difference is that you get a copy of the reference.
So, on my overloaded constructor, I am accepting a reference to an instance of a System.Net.Sockets.Socket, yes?
Yes, but the same would be true if you asked for it ByVal instead. The difference is that with ByVal you get a copy of the reference — you have new variable. With ByRef, it's the same variable.
It is my understanding that the instance in memory is copied
Nope. Only the reference is copied. Therefore, you're still working with the same instance.
Here's a code example that explains it more clearly:
Public Class Foo
Public Property Bar As String
Public Sub New(ByVal Bar As String)
Me.Bar = Bar
End Sub
End Class
Public Sub RefTest(ByRef Baz As Foo)
Baz.Bar = "Foo"
Baz = new Foo("replaced")
End Sub
Public Sub ValTest(ByVal Baz As Foo)
Baz.Bar = "Foo"
Baz = new Foo("replaced")
End Sub
Dim MyFoo As New Foo("-")
RefTest(MyFoo)
Console.WriteLine(MyFoo.Bar) ''# outputs replaced
ValTest(MyFoo)
Console.WriteLine(MyFoo.Bar) ''# outputs Foo
My understanding has always been that the ByVal/ByRef decision really matters most for value types (on the stack). ByVal/ByRef makes very little difference at all for reference types (on the heap) UNLESS that reference type is immutable like System.String. For mutable objects, it doesn't matter if you pass an object ByRef or ByVal, if you modify it in the method the calling function will see the modifications.
Socket is mutable, so you can pass any which way you want, but if you don't want to keep modifications to the object you need to make a deep copy yourself.
Module Module1
Sub Main()
Dim i As Integer = 10
Console.WriteLine("initial value of int {0}:", i)
ByValInt(i)
Console.WriteLine("after byval value of int {0}:", i)
ByRefInt(i)
Console.WriteLine("after byref value of int {0}:", i)
Dim s As String = "hello"
Console.WriteLine("initial value of str {0}:", s)
ByValString(s)
Console.WriteLine("after byval value of str {0}:", s)
ByRefString(s)
Console.WriteLine("after byref value of str {0}:", s)
Dim sb As New System.Text.StringBuilder("hi")
Console.WriteLine("initial value of string builder {0}:", sb)
ByValStringBuilder(sb)
Console.WriteLine("after byval value of string builder {0}:", sb)
ByRefStringBuilder(sb)
Console.WriteLine("after byref value of string builder {0}:", sb)
Console.WriteLine("Done...")
Console.ReadKey(True)
End Sub
Sub ByValInt(ByVal value As Integer)
value += 1
End Sub
Sub ByRefInt(ByRef value As Integer)
value += 1
End Sub
Sub ByValString(ByVal value As String)
value += " world!"
End Sub
Sub ByRefString(ByRef value As String)
value += " world!"
End Sub
Sub ByValStringBuilder(ByVal value As System.Text.StringBuilder)
value.Append(" world!")
End Sub
Sub ByRefStringBuilder(ByRef value As System.Text.StringBuilder)
value.Append(" world!")
End Sub
End Module
Think of C, and the difference between a scalar, like int, and an int pointer, and a pointer to an int pointer.
int a;
int* a1 = &a;
int** a2 = &a1;
Passing a is by value.
Passing a1 is a reference to a; it is the address of a.
Passing a2 is a reference to a reference; what is passed is the address of a1.
Passing a List variable using ByRef is analogous to the a2 scenario. It is already a reference. You are passing a reference to a reference. Doing that means that not only can you change the contents of the List, you can can change the parameter to point to an entirely different List. It also means you can not pass a literal null instead of an instance of List