Robert Giesecke's Unmanaged Exports - OUT strings - c#

I'm working from VBA into .NET.
I have a working version of an interface using CLI and stdcall
I'm trying to remove the resulting dependency on the C++ 2015 runtime, and it looks like I can do it using UnmanagedExports.
But I have a couple of questions.
Can I just use "ref string" as a parameter and have it work?
If so, can I replace it with "out string"?
In either cases, do I have to do any string/string length management?
I currently pass a couple of callbacks in as "int". From an example I saw elsewhere, it looks like on the C# side, using this, I should be able to replace those parameters with Func for a callback such as Function A(p1 as String, p2 as Long, p3 as String) as Long
Any advice would be much appreciated.

You need a combination of StrPtr, possibly StrConv on the Access side, and IntPtr on the .NET side:
'VBA7
Private Declare PtrSafe Function Command Lib "External.dll" (ByVal CommandName As String, ByVal Result As LongPtr, ByRef ResultLength As Long) As Long
'VBA pre7
Private Declare Function Command Lib "External.dll" (ByVal CommandName As String, ByVal Result As Long, ByRef ResultLength As Long) As Long
'Example to use.
'Result will be up to "i" characters - no new string involved
Dim i As Long, x As Long, strResult As String
i = 100
strResult = Space(i)
x = Command(CommandName, Arguments, StrPtr(strResult), i)
If you use StrConv, the string type is up to you. If you don't, the pointer will be pointing to a Unicode array.
C# side:
[DllExport("Command", CallingConvention.StdCall)]
public static int Command(string commandName, string arguments, IntPtr result, out /*or ref*/ int resultLength)
{
string inputStr = Marshal.PtrToStringUni(result); //Unicode
resultLength = inputStr.Length;
int x = MainFunc.Command(commandName, arguments, ref inputStr);
if(null == inputStr)
{
inputStr = "";
}
if(inputStr.Length > resultLength)
{
inputStr = inputStr.Substring(0, resultLength);
}
byte[] outputBytes = Encoding.Unicode.GetBytes(inputStr);
Marshal.Copy(outputBytes, 0, result, outputBytes.Length);
resultLength = inputStr.Length;
return x;
}

Related

Imported C-function works in C# but not in VB6

I'm working on a problem for a few days now and can't seem to find the answer. I have a third party dll written in C, which i have to use in an VB6 application.
The function inside dll looks something like this:
someFunction(WContext* context, const unsigned char* seed, int seedLength, const unsigned char* name, int nameLength,
const unsigned char* pin, int pinLength, const char* description)
I have an example written in c#. I tried it out and it works just fine. This is what it looks like in C#
[DllImport("Filename.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern int someFunction(IntPtr context, byte[] seed, int seedLength, byte[] name, int nameLength,
byte[] pin, int pinLength, string description)
This is later used in the example like this:
byte[] seed = Encoding.ASCII.GetBytes("seed")
byte[] name = Encoding.ASCII.GetBytes("name")
byte[] pin = Encoding.ASCII.GetBytes("1234")
string description = "description"
int result = someFunction(context, seed, seed.length, name, name.Length, pin, pin.Length, description)
Now this works just fine in C#. I get 0 as a result which means in that case that the operation was a success.
I want to make this work in VB6. I tried it with some other functions of the dll and they work just like they should. This one gives me a headache. Here is what I tried just like with any other function:
First I imported the function into my code so I could use it later. I did this with a few other functions the same way and it worked fine.
Private Declare Function someFunction Lib "C:\DllPath\Filename.dll" (ByVal context As Long, ByRef seed as Byte, _
ByVal seedLength As Integer, ByRef name As Byte, _
ByVal nameLength As Integer, ByRef pin As Byte, _
ByVal pinLength As Integer, ByVal description As String) As Integer
Next Step was for me to call the function. I did it this way:
(I do get the context from another function earlier so this already has a value. Other functions work fine with that variable)
Dim seed() As Byte
Dim seedLength As Integer
Dim name() As Byte
Dim nameLength As Integer
Dim pin() As Byte
Dim pin As Integer
Dim description As String
Dim result as Integer
seed = StrConv("seed", vbFromUnicode)
seedLength = UBound(seed) + 1
name = StrConv("name", vbFromUnicode)
nameLength = UBound(name) + 1
pin = StrConv("1234", vbFromUnicode)
pinLength = UBound(pin) + 1
description = "description"
result = someFunction(context, seed(0), seedLength, name(0), nameLength, pin(0), pinLength, description)
The value for result is 1. By the documentation I got this means invalid parameter. Now I researched a lot. Read that in VB6 I have to give the first element of the byte array just like I did in my code. Tried it first with the whole array, got the same result. I think it has something to do with the arrays since its the first function that I've taken over to my code that had those. I'm also not really that native with VB6 but I have to do this. Maybe some of you guys know why this doesn't work. Probably just a minor mistake by myself.
In VB6 it's as simple as this
Option Explicit
Private Declare Function someFunction Lib "C:\DllPath\Filename.dll" (ByVal context As Long, ByVal seed As String, _
ByVal seedLength As Long, ByVal name As String, _
ByVal nameLength As Long, ByVal pin As String, _
ByVal pinLength As Long, ByVal description As String) As Long
Private Sub Form_Load()
Dim context As Long
Dim seed As String
Dim name As String
Dim pin As String
Dim description As String
Dim result As Long
seed = "seed"
name = "name"
pin = "1234"
description = "description"
result = someFunction(context, seed, Len(seed), name, Len(name), pin, Len(pin), description)
End Sub
Just don't use Integer at all (only Longs) use ByVal ... As String for the runtime to do the Unicode->ANSI conversion and be done with it.
The original API is weirdly declared with unsigned char * pointer and separate int length parameter probably because the seed, name and pin strings can contain \0 embedded while description is plain zero-terminated string which explains the PITA w/ the C/C++ prototype.
VB6 strings are length-prefixed (and zero-terminated) from get go and can contain Chr$(0) by design so there is no need to take any additional measures like in C# sample w/ explicit byte[] arrays conversion and function prototype signature fiddling. Just use ByVal ... As String in the API declare.
When calling a declared API, VB6 will convert a String to an ANSI byte array automatically, so you can declare seed, name, and pin as ByVal Strings.
Description is a bit confusion to me though, since I don't see a length being passed for that one. Is this really a null-terminated string (pszDescription)? How does the API you are calling know how long this character buffer is?
You can try to change the function declaration in VB, making seed, name, and pin ByRef As Byte(),
note the parentheses:
Private Declare Function someFunction Lib "C:\DllPath\Filename.dll" (ByVal context As Long, ByRef seed as Byte(), _
ByVal seedLength As Integer, ByRef name As Byte(), _
ByVal nameLength As Integer, ByRef pin As Byte(), _
ByVal pinLength As Integer, ByVal description As String) As Integer
Then, when calling the function, pass the entire arrays, not the first elements:
result = someFunction(context, seed, seedLength, name, nameLength, pin, pinLength, description)

Return string from unmanaged dll to C#

I'm sorry to ask this here since I'm sure it must be answered "out there", but I've been stuck on this for several months now, and none of the solutions I've found have worked for me.
I have the following VB code that works:
Declare Function DeviceSendRead Lib "unmanaged.dll" (ByVal sCommand As String, ByVal sReply As String, ByVal sError As String, ByVal Timeout As Double) As Integer
Dim err As Integer
Dim outstr As String
Dim readstr As String
Dim errstr As String
outstr = txtSend.Text
readstr = Space(4000)
errstr = Space(100)
Timeout = 10
err = DeviceSendRead(outstr, readstr, errstr, Timeout)
and I am trying to implement it in a C# project. The best equivalent I have been able to find is:
[DllImport("unmanaged.dll")] public static extern int DeviceSendRead(String outstr, StringBuilder readstr, StringBuilder errstr, double Timeout);
int err;
StringBuilder readstr = new StringBuilder(4000);
StringBuilder errstr = new StringBuilder(100);
err = DeviceSendRead(txtSend.Text, readstr, errstr, 10);
However, when I run this, the application freezes and I must force quit it. By experimenting with ref and out, I have occasionally managed to make it crash rather than freeze, but the only "progress" I have achieved is to replace the dll function call with:
DeviceSendRead(txtSend.Text, null, null, 10);
This prevents the crash, but of course does nothing (that I can detect). I'm therefore assuming that it's the manner of passing the two return string parameters that is causing the problem. If anyone can suggest what I might be doing wrong, I'd be very happy to hear it. Thanks.
I have reached an answer, which I will record here for completeness, with grateful thanks to all those who pointed me in the right direction.
According to this post elsewhere, the use of .NET Reflector on similar VB code suggests the need to use the string type in place of my StringBuilder, as suggested here by Alex Mendez, JamieSee and Austin Salonen, together with explicit marshaling, as suggested by Nanhydrin, but utilising the unmanaged type VBByRefStr rather than AnsiBStr. The final key to the puzzle is that the string parameter then needs to be passed by reference using the ref keyword.
I can confirm that this works, and that my final working C# code is therefore:
[DllImport("unmanaged.dll", CharSet = CharSet.Ansi)]
public static extern short DeviceSendRead(
[MarshalAs(UnmanagedType.VBByRefStr)] ref string sCommand,
[MarshalAs(UnmanagedType.VBByRefStr)] ref string sReply,
[MarshalAs(UnmanagedType.VBByRefStr)] ref string sError,
double Timeout);
short err;
string outstr = txtSend.Text;
string readstr = new string(' ', 4000);
string errstr = new string(' ', 100);
err = DeviceSendRead(ref outstr, ref readstr, ref errstr, 10);
I hope this is useful to others facing a similar issue.
Try this:
[DllImport("unmanaged.dll")]
public static extern int DeviceSendRead(string outString, string readString, string errorString, double timeout);
int err;
string outstr;
string readstr;
string errstr =
outstr = txtSend.Text;
readstr = new string(' ', 4000);
errstr = new string(' ', 100);
double timeout = 10;
err = DeviceSendRead(outstr, readstr, errstr, timeout);
Try this as an equivalent:
string readstr = new string(' ', 4000);
string errstr = new string(' ', 1000);
Default Marshalling for strings
Default Marshalling behaviour
You may need to be more specific in your dllimport declaration and add in some MarshalAs attributes, if you have more details on what type of strings the called function is expecting (Ansi, Unicode, null terminated, etc.) then that would help.
In fact it expecting null terminated strings could perhaps explain why it's hanging rather than erroring out.
[DllImport("unmanaged.dll", EntryPoint="DeviceSendRead")]
public static extern int DeviceSendRead(string outString, [MarshalAs(UnmanagedType.AnsiBStr)]string readString, string errorString, double timeout);
You might also need to explicitly state that your parameters are input, output, or both by using the parameter attributes [In, Out].
[DllImport("unmanaged.dll", EntryPoint="DeviceSendRead")]
public static extern int DeviceSendRead(string outstr, string readstr, string errstr, double Timeout);
You cannot marshal a StringBuilder here. There are some rules to follow for marshalling StringBuilder (see CLR Inside Out: Marshaling between Managed and Unmanaged Code):
StringBuilder and Marshaling
The CLR marshaler has built-in knowledge of the StringBuilder type and
treats it differently from other types. By default, StringBuilder is
passed as [InAttribute, OutAttribute]. StringBuilder is special
because it has a Capacity property that can determine the size of the
required buffer at run time, and it can be changed dynamically.
Therefore, during the marshaling process, the CLR can pin
StringBuilder, directly pass the address of internal buffer used in
StringBuilder, and allow the contents of this buffer to be changed by
native code in place.
To take full advantage of StringBuilder, you'll need to follow all of
these rules:
1.Don't pass StringBuilder by reference (using out or ref). Otherwise, the CLR will expect the signature of this argument to be wchar_t **
instead of wchar_t *, and it won't be able to pin StringBuilder's
internal buffer. Performance will be significantly degraded.
2.Use StringBuilder when the unmanaged code is using Unicode. Otherwise, the CLR will have to make a copy of the string and convert
it between Unicode and ANSI, thus degrading performance. Usually you
should marshal StringBuilder as LPARRAY of Unicode characters or as
LPWSTR.
3.Always specify the capacity of StringBuilder in advance and make sure the capacity is big enough to hold the buffer. The best practice
on the unmanaged code side is to accept the size of the string buffer
as an argument to avoid buffer overruns. In COM, you can also use
size_is in IDL to specify the size.
Rule 3 doesn't seem like it is satisied here.

How to call this DLL function from either C++ / C#

I'm taking a lot of time trying to figure this out, so I thought I could get some help here.
Basically I have a DLL function declared like this in IDL:
[id(1), helpstring("method findFile")] HRESULT findFile(
[in] BSTR fileName,
[out] LONG* someValue
);
How exactly do I declare and invoke from either C++ / C#?
Note: there is a VB6 app that successfully invokes the function. Declaration is:
Private Declare Function findFile Lib "thedll.dll" ( _
ByVal fileName As String, _
ByRef someValueAs Long _
)
The call:
Dim a As String
Dim b As Long
Dim r As long
a = "image.jpg"
b = -1
r = findFile(a, b)
Addendum:
I cannot guarantee that the VB6 code looks like that because I have the executable, I was only told what that portion looks like, so maybe you guys are right and it doesn't match. I did author the C++ DLL, and now I need to get together some code myself that successfully calls the DLL in order to try out stuff and not depend on that exe.
C++ implementation of the DLL function looks like this:
STDMETHODIMP CFinder::findFile(BSTR fileName, LONG* someValue)
{
*someValue = 8;
return S_OK;
}
Untested C# declaration:
[DllImport("thedll.dll", SetLastError=true)]
static extern int findFile([MarshalAs(UnmanagedType.BStr)]string fileName, out int someValue);

C# Dll Invoke String Encoding Problem

StringBuilder codeline = new StringBuilder(100);
protected virtual int OnCodeLine(int code, int docId, ref StringBuilder codeline)
{
////
}
This is what i get with *ref StringBuilder*
ĞĞÑĞÒĞÓĞÔĞÕĞÖĞ×ĞØĞÙĞÚĞÛĞÜĞİĞŞĞßĞàĞáĞâĞãĞäĞåĞæĞçĞèĞéĞêĞëĞìĞíĞîĞïĞğĞñĞòĞóĞôĞõĞöĞ÷ĞøĞùĞúĞûĞüĞıĞşĞÿĞÑÑÑÑÑÑÑ Ñ
ÑÑÑ
ÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑÑ
and only with StringBuilder i only get 3
This is what its suppose to return
300 078 9059431
By the way this the MICR Code from Cheques
[DllImport("mflib.dll")]
public static extern int mfScanFeeder(int mode, int font, int timeout);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate int MFS100_CodeLineDelegate(int code, int docId, ref StringBuilder codeline);
public event MFS100_CodeLineDelegate MFS100_CodeLine;
private static MFS100_CodeLineDelegate cache_CodeLine;
Update : Here is the Original Code that works in vb6
Public Function MFS100_OnCodeline(ByVal code As Long, ByVal docId As Long, ByVal codeline As String) As Long
Dim i As Integer
WriteEvent "OnCodeline:"
WriteEvent " code = " & code
WriteEvent " docId = " & docId
WriteEvent " codeline = " & codeline
MFS100_OnCodeline = -1 ' -1 means: sorting will be done by mfSort()
g_codeline = codeline
CScannerForm.TmrSort.enabled = True
End Function
Update 2
mfSetEvent((int)EventEnum.E_EVENT_CODELINE, cache_CodeLine);
[DllImport("mflib.dll")]
private static extern int mfSetEvent(int eventID, Delegate callsback);
When i use StringBuilder with ref i get a string that have
32361 length. Without ref i get
only the first value of the
string.
OnCodeLine is for the callback
from the scanner device. What is the
problem ?
You should not pass the StringBuilder by ref. When you do that you say you only get one character back. That's the hallmark of expecting ANSI encoding but actually receiving Unicode (UTF-16). Fix it by specifying the character set:
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet=CharSet.Unicode)]
public delegate int MFS100_CodeLineDelegate(int code, int docId, StringBuilder codeline);
I don't understand what are you trying to achieve, but if you want to collect a string generated inside OnCodeLine, you don't need to pass the StringBuilder by reference, as it is a reference type.
Just pass the StringBuilder without ref, populate it, and when you return you'll have the desired string in it.
Regarding what you get after calling the OnCodeLine, can you provide some info regarding the implementation?
Sorry, I didn't notice the PInvoke was involved!! :(

Converting a function from Visual Basic 6.0 to C# is throwing AccessViolationException

I'm converting a function from Visual Basic 6.0 as:
Declare Function RequestOperation Lib "archivedll" (ByVal dth As Long, ByVal searchRequestBuf As String, ByVal buflen As Long, ByVal FieldNum As Long, ByVal OP As Long, ByVal value As String) As Long
In C#, I'm declare the function as:
[DllImport("archivedll")]
public static extern int RequestOperation(int dth ,StringBuilder searchRequestBuf, int bufferLen, int fieldNum, int op, string value);
When call RequestOperation from C#, it throws an exception:
[System.AccessViolationException] =
{"Attempted to read or write protected
memory. This is often an indication
that other memory is corrupt."}
I have successful in calling many other functions like this, but only this function throws the exception.
This function is clearly not throwing an AccessViolationException - instead, it is generating an access violation fault by "attempting to read or write protected memory". .NET is translating that fault into an AccessViolationException.
You'll have to go figure out why it is "attempting to read or write protected memory". In particular, did you initialize the StringBuilder you are passing to it? Please post the code you use to call this method.
I think the StringBuilder in the function declaration has something to do with it. You should use just plain String instead.
/// Return Type: int
///dth: int
///searchRequestBuf: BSTR->OLECHAR*
///buflen: int
///FieldNum: int
///OP: int
///value: BSTR->OLECHAR*
[System.Runtime.InteropServices.DllImportAttribute("<Unknown>", EntryPoint="RequestOperation")]
public static extern int RequestOperation(int dth, [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.BStr)] string searchRequestBuf, int buflen, int FieldNum, int OP, [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.BStr)] string value) ;

Categories

Resources