vararg function with SAFEARRAY(VARIANT) parameter not callable from VBA - vba

I'm trying to define a function in a type library that takes a variable number of parameters and can be callable from VBA using ParamArray.
The entry in the type library is below.
[
helpstring("Get value from a Lookup table by an exact key."),
entry("UtilDll_LookupExact"),
vararg
]
HRESULT __stdcall LookupExact(
[out] LPVARIANT Result,
[in] LPVARIANTARG Table,
[in] LONG VarIndex,
[in] SAFEARRAY(VARIANT) Key,
[out, retval] VARIANT_BOOL *Found
);
I can see this VBA's object browser as:
Function LookupExact(Result, Table, VarIndex As Long, ParamArray Key() As Variant) As Boolean
So, at least the object browser seems to understand Key as ParmArray () Variant.
But when I call this function I get a compile error saying, "Compile Error: Function or Interface Marked as Restricted, or the Function Uses an Automation Type Not Supported in Visual Basic"
The closest question I found is below, but this case seems specific to managed code while I'm dealing with native code.
ParamArray Not Working With COM
How can I make a function in native code that has the ParamArray parameter and can be accessed via VBA?

I found the answer here:
http://computer-programming-forum.com/71-visual-basic-vb/a064ce72acaeb9d8.htm
Apparently, the Key parameter in IDL should have been:
[in] SAFEARRAY(VARIANT)* Key,
And C++ function's arguments type should be
SAFEARRAY** Key

Related

Option Strict On disallows late binding in vb.net

I'm using COM interface to 3rd part program to get information with my functions. (VS2017 and Framework 4.7.2).
I'm getting an error from Visual Studio: "Option Strict On disallows late binding" for the below function
'x, y, z, al, be, ga. as an array
Protected Friend Shared Function GetComputedBRFPos(ByVal bodyElement As IScrBody, ByVal index As Integer) As Array
Return bodyElement.getComputedBRFPos(p_index:=index)
End Function
It has a documentation at 3rd part tool i'm also writing the description.
VARIANTList getComputedBRFPos ()
Get current BRF position, creates an implicit solver if no solver is existing. Array elements: x, y, z, al, be, ga.
For an example i'm putting another function i'm using and getting no late binding error for below function.
Protected Friend Shared Function Get_sb_node_pos(ByVal bodyElement As IScrBody, ByVal childIndex As Integer) As Array
Return bodyElement.get_sb_node_pos(p_childIndex:=childIndex)
End Function
And it's description at documentation.
VARIANTList get_sb_node_pos (int childIndex)
Get all elements of
sb_node_pos as an array.
I think it causing for bodyElement.getComputedBRFPos(p_index:=index) "index" value but i don't know what's the exact problem and how to achieve.
From the documentation you posted, it seems like bodyElement.getComputedBRFPos doesn't take any parameters. In VB.NET, the () are optional for method without parameters. So your code end up looking like this.
Return bodyElement.getComputedBRFPos()(p_index:=index)
Which doesn't return an array but instead return an element of the array which is of type object.
You should remove the parameter, change the return type or show us the documentation of the method with the parameter you are trying to call.
Return bodyElement.getComputedBRFPos()

Does Replace$() function exist in Microsoft Access?

It works, but is not listed here:
https://msdn.microsoft.com/en-us/library/office/gg278535.aspx
... > Office shared > Office VBA language reference > Visual Basic conceptual topics
Returning Strings from Functions
and in the VBA editor:
(View) - Object Browser - VBA - Strings
This function is discussed in many forums, so people use it.
The answer to your question depends on what you mean by "... exist[s] in Access". If we open an Immediate window in Access' VBA development environment and run
?Replace$("I like tofu!", "tofu", "bacon")
we get
I like bacon!
so clearly the Replace$ function does "exist" in Access itself. However, if we try to use Replace$ in a query against an Access database from an external application (e.g., VBScript, .NET, ...) we'll get
Undefined function 'Replace$' in expression.
Adding to the confusion is the fact that the older "Jet" ODBC/OLEDB drivers did not support the Replace function (without the dollar sign) but the newer "ACE" ODBC/OLEDB drivers do. (Neither flavour supports Replace$.) So a query like
SELECT Replace([Name], 'Gordon ', 'Gord ') AS newName FROM ...
will work if we use the ODBC driver
Driver={Microsoft Access Driver (*.mdb, .accdb)}
but not if we use
Driver={Microsoft Access Driver (*.mdb)}
The functions in the VBA.Strings module are "special" in how they are handled internally by VBA. For most of them, there are actually 2 versions in the type library - a version that returns a String (ends with $), and a version that returns a Variant. Internally these are declared as a pair of functions - for example, Right (from the vbe7.dll TypeLib):
[entry(618), helpcontext(0x000f6ea5)]
BSTR _stdcall _B_str_Right(
[in] BSTR String,
[in] long Length);
[entry(619), helpcontext(0x000f656e)]
VARIANT _stdcall _B_var_Right(
[in] VARIANT* String,
[in] long Length);
The compiler apparently treats the $ similarly to a "type hint" internally, because (using the example above), there actually isn't a function Left$ defined in the TypeLib. In fact, there isn't a function declared as VBA.Strings.Right either. These live in a special restricted interface named _HiddenInterface:
[
odl,
uuid(1E196B20-1F3C-1069-996B-00DD010EF676)
]
interface _HiddenInterface {
...
[restricted, helpcontext(0x000f6d7c)]
void _stdcall Right();
...
};
Note that Right$ doesn't appear in the in the _HiddenInterface, nor do any of the other string returning functions. The VBA compiler uses the "function type hint" to forward the function call to either _B_str_Right or _B_var_Right.
By now you're probably wonder what this has to do with your question. The answer is that Replace actually doesn't have two different internal representations. It always returns a string, doesn't exist on the _HiddenInterface, and lives directly in the VBA.Strings module:
[entry(712), helpstring("Find and replace a substring within a string"), helpcontext(0x000f6522)]
BSTR _stdcall Replace(
[in] BSTR Expression,
[in] BSTR Find,
[in] BSTR Replace,
[in, optional, defaultvalue(1)] long Start,
[in, optional, defaultvalue(-1)] long Count,
[in, optional, defaultvalue(0),
custom(270D72B0-FFB8-11CF-A4BD-00A0C90F26EE, 1)
] VbCompareMethod Compare);
Basically, there is no Replace$ function at all. VBA is treating the $ as a type hint for the return value (which is always a String anyway). As far as the ODBC and OLE drivers are concerned, I would imagine (TBH I really haven't looked into it much) that they are restricted to the names that are exposed by the TypeLib and are not interpreted by the VBA runtime as forwards to different functions. They simply don't exist if you're doing IDispatch lookups on vbe7.dll.

Using success HRESULTs in COM

I have some functions in a COM interface that return different success values via the HRESULT, but using the space that is defined as "successful" (i.e. SUCCEEDED(hr) is non-zero).
For example
HRESULT MyMessageBox( BSTR title /*[in]*/, BSTR text /*[in]*/, long buttons /*[in]*/ );
a function that displays a dialog similar to a MessageBox; it returns an indicator of which button the user used to dismiss the dialog. (I defined an enum for this with values within the space of HRESULTs which is reserved for user-defined codes).
This is fine in a C++ client; however when VB is the client, or Java wrappers such as JACOB, it appears to intercept the HRESULT and there is no way that the client can tell which successful code occurred.
Is it actually a terrible design to have the HRESULT indicate anything other than 0 or exceptions ; should I instead make new functions that have an [out] parameter to get which button is used?
Most language runtimes will map an HRESULT to an exception, makes writing COM code a lot easier. And yes, they'll ignore positive values. They might have an escape for that, in .NET the [PreserveSig] attribute suppresses the exception mapping and exposes the HRESULT return value as an int.
But that's painful and unnecessary. They will also map an argument that you decorate as [out, retval] to the function return value. Which is what you are looking for here:
HRESULT MyMessageBox([in] BSTR title, [in] BSTR text, [in] long buttons,
[out,retval] long* result);
And now the client programmer can write something like this:
int result = yadayada.MyMessageBox("title", "text", 0);
Giving you the opportunity to use the HRESULT only for "function failed" return values, S_OK otherwise. You can further improve it by using enum types to name the legal button and return values.

Trying to call a C DLL from VB. Can't get one of the parameters working

Trying to set up a USB power strip.
Here's the documentation:
Initializes the Power USB API.
Name: InitPowerUSB
Parameters: model:returns the model number(1:basic, 2:digIO, 3:watchdog, 4:Smart), firmware: returns firmware version in ?.? format in a character string (major revision and minor revision)
Return: >0 if successful. Returns number of PowerUSB devices connected
C++ Example:
if (!m_pwrUSBInit)
{
int model; char firmware[8];
if ((ret=InitPowerUSB(&model, firmware)) > 0)
{
m_pwrUSBInit = 1;
m_numDevices = ret;
}
}
I have been trying to get this working with my VB6 code for around an hour now with no luck. The program either crashes, displays an error like Bad Calling Dll Convention, type mismatch, et cetera.
Here's what I have:
Public Declare Function InitPowerUSB Lib "PwrUSBDll.dll" (ByRef model As Integer, ByVal firmware As String) As Integer
Dim model As Integer
model = 0
Dim firmware As String
firmware = ""
If (InitPowerUSB(model, firmware)) > 0) Then
EndIf
I have tried changing firmware to byte arrays, byref, string, integer, long, etc. It just doesn't seem to want to run.
Does anyone know of a solution to this problem? Thanks
I can't answer the rest of your function signature woes since I don't have any documentation for your PwrUSBDll.dll.
However "Bad DLL calling convention" errors generally mean you have a CDecl entrypoint and VB6 can only call those with some help.
There are a couple of fixes.
The obvious one is to modifiy the source and recompile that DLL using StdCall instead.
Another is to create a type library for that DLL, which helps inform VB6 about the issue and resolves it.
Then you have the option of using VB6's undocumented CDecl decorator:
Public Declare Function InitPowerUSB CDecl Lib "PwrUSBDll.dll" ( _
ByRef model As Integer, _
ByVal firmware As String) As Integer
However the downside is that this will not work when run within the IDE, nor will it work when compiled to p-code. The p-code interpreter doesn't process this keyword.
So you could just bypass it in IDE runs and supply dummy results for testing, or you can create a small wrapper DLL in VB6 that you separately compile to native code.
Caveats:
For this to solve your problem we'd have to assume you are passing correct data types in that argument list. A C++ int is a VB6 Long. You are probably better off passing a VB6 Byte array ByRef for that char[8] unless this is a Unicode DLL entrypoint. The function return value is also most likely Long.

Call a function in a C DLL from VB: Access Violation

I'm trying to call a Dll function which looks like this (in the C/C++ Dll):
__declspec(dllexport) void execute(char** ReturnMsg, char* serverAddress, char* commandLine)
The VB 'wrapper' function looks like:
<DllImport("TPClient.dll", EntryPoint:="execute", CallingConvention:=CallingConvention.Cdecl, CharSet:=CharSet.Auto, ExactSpelling:=True)> _
Public Shared Sub tg_execute(<Out()> <MarshalAs(UnmanagedType.LPStr)> ByRef returnString As System.Text.StringBuilder, _
<MarshalAs(UnmanagedType.LPStr)> ByVal serverAddress As String, _
<MarshalAs(UnmanagedType.LPStr)> ByVal commandLine As String)
End Sub
The parameters are:
returnString: a string I need to get back from the function, result of the command sent;
serverAddress: a string, input only (an IP or DNS name); and
commandLine: a string, input only (any command)
To call the function, I make a StringBuilder object with some sufficient capacity to use as the returnString variable:
Dim returnString As New System.Text.StringBuilder(128)
tg_execute(returnString, TextBox_serverName.Text.Trim, TextBox_Command.Text.Trim)
When I run the code, I do get the expected string in the returnString (as I can see in the debugger), however I also get an AccessViolationException. So, I get for example "2.6.30.8-x86" in returnString when I use the command "uname -r" in commandLine. But the code hangs due to the memory error.
Now I'm not too familiar with VB and P/Invoke, and I had to do some trial and error to get the arguments passed to the DLL (which I'm also writing and debugging). This is also how I ended up using the "MarshalAs(UnmanagedType.LPStr)" attributes. However now I don't know why I'm getting these memory errors.
I made some other attempts using IntPtr arguments, but I also couldn't get this working and gave up on that approach, as to my understanding the marshaling should be handled automatically (is that correct?).
Any help is much appreciated.
The return value char** ReturnMsg would suggest that ReturnMsg is a pointer to C string. This would imply that the native code was in charge of allocating the buffer. So StringBuilder is not appropriate here.
There is not actually enough information here to know how to call this function. What is missing is knowledge of which party is responsible for deallocating the string. It could be either party and I'm going to assume that the C code will do so, probably by means of the strings being statically allocated, e.g. constants.
Now, I have no experience with VB p/invoke so I hope you don't mind if I give you a C# version. I expect you can translate easily enough.
[DllImport("TPClient.dll", CallingConvention=CallingConvention.Cdecl,
CharSet=CharSet.Ansi, EntryPoint="execute", ExactSpelling=true)]
private static void tg_execute(out IntPtr returnString,
string serverAddress, string commandLine)
You call the function like this:
IntPtr returnStringPtr;
tg_execute(out returnStringPtr, serverAddress, commandLine);
string returnString = Marshal.PtrToStringAnsi(returnStringPtr);
Note that your character set was incorrect in the question. You have to use Ansi because the native code uses char. I also think that your MarshalAs attributes are spurious since you are just re-stating the default marshalling for those parameter types.
Now, if the native code expects the caller to deallocate the memory, then the native code would have to export a function to do so. If that's the case then you would call it passing returnStringPtr by value.