How do I pass an array of arguments ByRef with CallByName?
Asked Answered
S

2

6

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?

Slaughter answered 25/5, 2016 at 8:59 Comment(0)
A
2

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.

Abstergent answered 25/5, 2016 at 13:18 Comment(7)
I believe your paragraph ending in "...it has to assume that it can't change them." to be incorrect. When using CallByName directly in VBA ByRef arguments work as expected, so there cannot be an inherent limitation in the CallByName implementation. IMO the problem is in passing the Args/Array to the declared function.Eiser
I agree byRef arguments work directly with CallByName so it seems strange that there would be a limitation. Is there a way round this? I need to pass an array of argument as I have no idea how many there will be until runtimeSlaughter
@Eiser - Direct calls to CallByName aren't marshaled through the COM boundary.Abstergent
Unfortunately the Methods are a proprietary external library as they allow me to connect to an external piece of software through COM object, so I cannot refactor or create a class.Slaughter
@Slaughter Wrap the arguments in an Array() call and have the function accept a regular array as its argument rather than ParamArrayGranny
"The rtcCallByName function itself doesn't (and can't) know which of the parameters [...] are declared ByRef or ByVal" (emphasis mine) - this could be more clear. Variants in VBA contain a hidden VT_BYREF flag in the upper bytes of the VarType that indicates whether they are passed ByVal/ByRef. So the array given to rtcCallByName can indicate how the arguments were passed. It just happens that in vba whenever you assign an array to a new variable/function argument, a shallow copy is made that converts the members to ByVal...Tavish
...However rtcCallByName doesn't have to accept the args argument as an array. You can also supply a pointer to the array: ByVal pArgs As LongPtr, or the array first member ByRef, which is the same thing but declared like:ByRef firstArg As Variant. The issue is ParamArray is not quite the same as a normal VBA array, so you have to make a copy explicitly, with an intermediate variable, or implicitly (as in OP's code). That's where this answer comes in, which copies the paramarray to an intermediate variable while preserving the VT_BYREFsTavish
T
1

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

Tavish answered 25/5, 2016 at 8:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.