2

I have some problem with marshalling and PInvoke I have to develop some kind of driver for existing native application (Oracle Siebel CRM, call center integration interface). Sources of the application is black box from my point of view. Here is the signature of two structures (as defined in API description) and API function which is working with they:

struct ISC_KeyValue /* Key-Value element */
{
ISC_STRING paramName;
ISC_STRING paramValue;
};

struct ISC_KVParamList /* List of Key-Value parameter */
{
struct ISC_KeyValue* dataItems;
long len;
};

ISCAPI ISC_RESULT CreateISCDriverInstance
/* in */(const ISC_STRING mediaTypeStr,
/* in */ const ISC_STRING languageCode,
/* in */ const ISC_STRING connectString,
/* in */ const struct ISC_KVParamList* datasetParams,
/* out */ ISC_DRIVER_HANDLE* handle);

ISC_STRING here is wide character C++ string(2 bytes Unicode). ISC_RESULT is integer value similar to HRESULT type. I have a problem with marshaling of ISC_KVParamList* datasetParams in to CreateISCDriverInstance function and i need help with it.

Here is my vision on how I should marshal this:

   [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct IscKeyValue
    {
          [MarshalAs(UnmanagedType.LPWStr)]
          public string ParamName;
          [MarshalAs(UnmanagedType.LPWStr)]
          public string ParamValue;
    }

  [StructLayout(LayoutKind.Sequential)]
public struct IscKvParamList
{

    public IntPtr DataItems;
    [MarshalAs(UnmanagedType.I4)]
    public int Len;
}

And the function itself:

    [DllExport("CreateISCDriverInstance", CallingConvention = CallingConvention.StdCall)]
    public static int CreateIscDriverInstance([MarshalAs(UnmanagedType.LPWStr)] string mediaTypeStr,
        [In,MarshalAs(UnmanagedType.LPWStr)] string languageCode,
        [In,MarshalAs(UnmanagedType.LPWStr)] string connectString,
        [In, Out] ref IscKvParamList dataParams, [Out] IntPtr handle)
        {
 var datasetParams =(IscKvParamList)Marshal.PtrToStructure(dataParams,typeof(IscKvParamList));
             // Some code here
            }

When i'm trying to perform PtrToStructure conversion I get the exception which is saying that "The structure must not be a value class."

Question: How to improve marshalling of dataParams argument

p.s. Additional details : 1. I'm sure that charset is Unicode 2. I do not know the size of array and I cannot marshal it by value. 3. Also, i have example implementation of such driver which was written a long time ago in Delphi:

TNamedParam = record 
Name: WideString;
Value: WideString; 
end; 

TNamedParamList = record
Params: packed array of TNamedParam;
Count: Cardinal;
end;

function CreateISCDriverInstance(const AMediaTypeStr, ALanguageCode, AConnectString: PWideChar; const AParams: TISCNamedParamList; out ADriverHandle: THandle):
HRESULT; begin try ADriverHandle := icISCCommunicationDriver.CreateInstance(AParams).Handle; 
Result := SC_EC_OK;
except Result := SC_EC_DRIVER_CREATION_ERR;
end;
end; 

And parsing of AParams:

procedure JoinISCNamedParamList(const AParamList: TNamedParamList; var LParamList: TISCNamedParamList);
  var i:Integer;
begin
  LParamList.Count := AParamList.Count;
  SetLength(LParamList.Params, LParamList.Count);
  for I := 0 to Pred(AParamList.Count) do
  begin
    LParamList.Params[i].Name := PWideChar(AParamList.Params[i].Name);
    LParamList.Params[i].Value := PWideChar(AParamList.Params[i].Value);
  end;
end;

It's possible to say that Layout of structs is Sequential here.

ArtemKunik
  • 13
  • 1
  • Who knows. I do not have C/C++ code, it's Siebel CRM and I do not have sources. But I figured out(after some tryies) that ISC_STRING - it's C++ wide string (WCHAR), becuase i'm getting correct values for this three parameters: [MarshalAs(UnmanagedType.LPWStr)] string mediaTypeStr, [In,MarshalAs(UnmanagedType.LPWStr)] string languageCode, [In,MarshalAs(UnmanagedType.LPWStr)] string connectString, – Anton Antonenko Nov 20 '14 at 08:09
  • I don't see any reason to write such code. I will not get any information about what coming from caller(Siebel CRM) side. But i got some kind of example of such driver, which is ~10 years old and written on Delphi. As far as i see - caller side is responsible to allocate/release memory in such case. – Anton Antonenko Nov 20 '14 at 08:28
  • Here is implementations of such method on Delphi: TNamedParam = record Name: WideString; Value: WideString; end; TNamedParamList = record Params: packed array of TNamedParam; Count: Cardinal; end; function CreateISCDriverInstance(const AMediaTypeStr, ALanguageCode, AConnectString: PWideChar; const AParams: TISCNamedParamList; out ADriverHandle: THandle): HRESULT; begin try ADriverHandle := TicISCCommunicationDriver.CreateInstance(AParams).Handle; Result := SC_EC_OK; except Result := SC_EC_DRIVER_CREATION_ERR; end; end; – Anton Antonenko Nov 20 '14 at 08:32
  • Why did you use FieldOffset? Why did you decide to use a SAFEARRAY? Why did you add lots of code in comments that is unreadable? What is `ISC_STRING`? What about the other types mentioned in the C++ code? If you improved the question it should be easy enough to answer. – David Heffernan Nov 20 '14 at 09:26
  • You're right, this is not the SAFEARREY. Now i'm trying to marshal the IscKvParamList.DataItems as IntPtr and then use Marshal.PtrToStructure. – Anton Antonenko Nov 20 '14 at 09:33
  • This is really confused and confusing. I can't help any more until there is more detail. However, it's not going to be difficult to solve the problem when that arrives. – David Heffernan Nov 20 '14 at 09:36
  • That's better but you still did not show the declarations of all the types: `ISC_STRING` and so on. – David Heffernan Nov 20 '14 at 09:42
  • I don't have such information. Here is API Oracle documentation https://www.dropbox.com/s/fnm5yigrrblysih/SiebCTIAdm.pdf?dl=0 ( page 272 ). There is no declaration of ISC_STRING. But i'm sure that mediaTypeStr,languageCode, connectString input arguments marshaling is working( it's marshaled ans UnmanagedType.LPWStr). – Anton Antonenko Nov 20 '14 at 09:48
  • I don't want to look at that. Sorry. I'll help if you dig out the missing declarations and add them to the question. I cannot believe that you have `ISC_KeyValue` but not `ISC_STRING`. It's not going to be hard. The Delphi code is clear enough. It's odd though because it uses WideString which is actually a BSTR and a dynamic array. Implementation details mean that is compatible with the C++ pointer to array. Anyway, you must have the information. Please add it. – David Heffernan Nov 20 '14 at 09:51
  • It may seem strange that I haven't such information, if you have not faced with the documentation from Oracle before. But it is so :( You can ensure in that if you'll at book provided. Lack of information it's main problem for me. I'm terrible sorry – Anton Antonenko Nov 20 '14 at 09:59
  • I've done my best in my answer. Sorry for being picky but as I'm sure you know, interop is all about detail. – David Heffernan Nov 20 '14 at 10:07
  • David, I really appreciate your help. I'll try your solution now. – Anton Antonenko Nov 20 '14 at 11:29

1 Answers1

1

The information in the question is incomplete. A websearch reveals code that says:

typedef wchar_t ISC_CHAR
typedef ISC_CHAR* ISC_STRING; 

That tallies with what you indicate in the question. So, let's go with that. However, it would have been much better if you could have provided that information which can be found in one of your header files.

Naively your structs should be:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct IscKeyValue
{
    public string ParamName;
    public string ParamValue;
}

[StructLayout(LayoutKind.Sequential)]
public struct IscKvParamList
{
    public IscKeyValue[] DataItems;
    public int Len;
}

No need for the MarshalAs that you used because the default marshalling suffices.

But the marshaller just won't deal with the array inside the struct. So I think you'll need to do it like this:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct IscKeyValue
{
    public IntPtr ParamName;
    public IntPtr ParamValue;
}

[StructLayout(LayoutKind.Sequential)]
public struct IscKvParamList
{
    public IntPtr DataItems;
    public int Len;
}

As for the function, it translates as:

[DllExport("CreateISCDriverInstance", CallingConvention = CallingConvention.StdCall,
    CharSet = CharSet.Unicode, ExactSpelling = true)]
public static int CreateIscDriverInstance(
    string mediaTypeStr,
    string languageCode,
    string connectString,
    [In] ref IscKvParamList dataParams, 
    out IntPtr handle
);

And now the fun. Preparing the IscKvParamList parameters.

Start with an array of IscKeyValue:

IscKeyValue[] KeyValueArr = new IscKeyValue[...];
for (int i = 0; i < KeyValueArr.Length; i++)
{
    KeyValueArr[i].ParamName = Marshal.StringToCoTaskMemUni(...);
    KeyValueArr[i].ParamValue = Marshal.StringToCoTaskMemUni(...);
}

Now the IscKvParamList. I think that I would pin the array. Like this:

GCHandle ArrHandle = GCHandle.Alloc(KeyValueArr, GCHandleType.Pinned);
try
{
    IscKvParamList dataParams;
    dataParams.DataItems = ArrHandle.AddrOfPinnedObject();
    dataParams.Len = KeyValueArr.Length;       
    int retval = CreateIscDriverInstance(
         mediaTypeStr,
         languageCode,
         connectString,
         ref dataParams,
         out handle
    );
}
finally
{
    ArrHandle.Free();
}

You must make sure that you remember to deallocate the unmanaged memory allocated in the calls to Marshal.StringToCoTaskMemUni.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • You're right and that's is the correct solution! Here is only one small detail in the signature of the CreateISCDriverInstance: for mediaTypeStr, languageCode, connectString attribute MarshalAs(UnmanagedType.LPWStr) is required. Without it only first character in string will be passed. Thank you very much for your help! – Anton Antonenko Nov 20 '14 at 12:10
  • Let me know what I got wrong and I'll fix it. Yes, I forgot the CharSet. I'll fix that. Better that way than MarshalAs I think. – David Heffernan Nov 20 '14 at 12:13
  • Here is correctly working signature of CreateISCDriverInstance: [DllExport("CreateISCDriverInstance", CallingConvention = CallingConvention.StdCall)] public static int CreateIscDriverInstance([MarshalAs(UnmanagedType.BStr)] string mediaTypeStr, [MarshalAs(UnmanagedType.LPWStr)] string languageCode, [MarshalAs(UnmanagedType.LPWStr)] string connectString, [In] ref IscKvParamList datasetParams, [Out] IntPtr handle) – Anton Antonenko Nov 20 '14 at 12:34
  • I really don't think there are any BSTR here FWIW, looking at you latest code with comments. – David Heffernan Nov 20 '14 at 12:39
  • CharSet attribute just cannot be applied with DllExport(seems that author of DllExport library forgot to add this possibility). Only CallingConversion can be applied with DllExport. But i think that you're rigth. – Anton Antonenko Nov 20 '14 at 12:42
  • You can certainly use CharSet. Perhaps you need ExactSpelling = true too. Yes that will be it. Try now. MarshalAs works, but I prefer CharSet here. Anyway, choice is yours. – David Heffernan Nov 20 '14 at 12:44