Search code examples
c#c++comvariant

Exception Passing SAFEARRAY in VARIANT to C# From C++ COM Server


I have spent the last day searching documentation, reviewing forum posts, and googling to try to do something that I am guessing could be easily done with the right information.

I have a very large existing C++ application that has an already defined COM server with many methods exposed. I am trying to use those COM methods in a C# application (I am experienced in C++, but a C# newbie).

So in my VS2010 C# application, I add the COM server as a reference. COM Methods are visible in the object browser, and passing single-valued strings, floats, and ints seems to work fine.

But I am stumped trying to read SAFEARRAY values passed out of the C++ COM server into the C# application. Eventually I need to pass string arrays from C++ server to the C# app, but in just testing passing an array of floats, I have code that builds but fails with the following exception when I try to cast the System.Object containing the array of floats to (float[]) ,

"exception {System.InvalidCastException: Unable to cast object of type 'System.Object[]' to type 'System.Single[]'."

With intellisence I can see that the Object contains the correct 8760 long array of floats, but I am unable to access that data in C#.

Here is the code on the C# side (d2RuleSet is an interface defined in the DOE2Com COM server). The exception above is thrown on the last line below.

DOE2ComLib.DOE2Com d2RuleSet;

d2RuleSet = new DOE2ComLib.DOE2Com();

System.Int32 i_Series =0;

System.Object pv_WeatherData;
float[] faWeatherData;

iOut = d2RuleSet.GetWeatherData(i_Series, out pv_WeatherData);
Type typeTest;
typeTest = pv_WeatherData.GetType();
int iArrayRank = typeTest.GetArrayRank();
Type typeElement = typeTest.GetElementType();                    
faWeatherData = (float[])pv_WeatherData;

Below is the section in the idl file defining the C++ COM method

HRESULT GetWeatherData( [in] int iSeries, [out] VARIANT* pvWeatherData,    [out,retval]      int * piErrorCode);

Below is the C++ code where the VARIANT data is loaded.

void CDOE2BaseClass::GetWeatherData( int iSeries, VARIANT* pvWeatherData, int*   piErrorCode)
{
    *piErrorCode = 0;

    if (iSeries < 0 || iSeries >= D2CWS_NumSeries)
       *piErrorCode = 1;
    else if (m_faWeatherData[iSeries] == NULL)
       *piErrorCode = 3;
    else
   {
       SAFEARRAYBOUND rgsaBound;
       rgsaBound.lLbound = 0;
       rgsaBound.cElements = 8760;

      // First lets create the SafeArrays (populated with VARIANTS to ensure    compatibility with VB and Java)
       SAFEARRAY* pSAData = SafeArrayCreate( VT_VARIANT, 1, &rgsaBound );
       if( pSAData == NULL ) {
 #ifndef _DOE2LIB
          _com_issue_error( E_OUTOFMEMORY);
#else
//RW_TO_DO - Throw custom Lib-version exception
          OurThrowDOE2LibException(-1,__FILE__,__LINE__,0,"OUT OF MEMORY");
 #endif //_DOE2LIB
       }

      for (long hr=0; hr<8760; hr++)
      {
          COleVariant vHrResult( m_faWeatherData[iSeries][hr] );
          SafeArrayPutElement( pSAData, &hr, vHrResult );
       }

      // Now that we have populated the SAFEARRAY, assign it to the VARIANT pointer that we are returning to the client.
       V_VT( pvWeatherData ) = VT_ARRAY | VT_VARIANT;
       V_ARRAY( pvWeatherData ) = pSAData;
    }
}

Thanks in advance for help with this problem, I feel like I spent too much time on what should be a simple problem. Also please post up any links or books that that cover interop between native C++ and C# well (I think I have already ping-ponged through most of the Visual Studio/MSDN documentation, but maybe I missed something there too).

-----------------End Of Original Question------------------------------------------------------ I am editing to post the code from phoog's successful solution below, so others can read and use it.

int iOut = 0;
System.Int32 i_Series =0;
System.Object pv_WeatherData = null;

iOut = d2RuleSet.GetWeatherData(i_Series, out pv_WeatherData);
Type typeTest;
typeTest = pv_WeatherData.GetType();
int iArrayRank = typeTest.GetArrayRank();
Type typeElement = typeTest.GetElementType();

//float[] faWeatherData = (float[])pv_WeatherData;
float[] faWeatherData = ConvertTheArray((object[])pv_WeatherData);

....

float[] ConvertTheArray(object[] inputArray)
{
   float[] result = new float[inputArray.Length];
   for (var index = 0; index < result.Length; index++)
      result[index] = (float)inputArray[index];
   return result;
}

Solution

  • The marshalled SAFEARRAY is a System.Object[]; you can't reference-convert that to a System.Single[]. You have to cast the individual elements. This would work, assuming that all the elements of the argument array are in fact boxed floats:

    float[] ConvertTheArray(object[] inputArray)
    {
        float[] result = new float[inputArray.Length];
        for (var index = 0; index < result.Length; index++)
            result[index] = (float)inputArray[index];
        return result;
    }
    

    You could do that with a lot less typing using Linq, but as you're a C# newbie I thought a more basic solution might be better.

    EDIT

    Since you indicated in your comment that your object[] is referenced as an object, here's the usage example:

    object obj = GetMarshalledArray();
    float[] floats = ConvertTheArray((object[])obj);
    

    EDIT 2

    A shorter solution using Linq:

    float[] ConvertTheArray(object[] inputArray)
    {
        return inputArray.Cast<float>().ToArray();
    }