Click or drag to resize

Using Object Parameters

Some instrument operations produce several related results. Consider, for instance, an operation that performs a CDMA measurement and returns IQ data. Each operation yields four pieces of information:

  • The data type (as a string).

  • The spread factor (as an integer).

  • The code number (as an integer).

  • The channel amplitude (as a floating point number).

Since all of these items are returned from a single operation, the driver needs to expose a single method that returns all of these items. A naive design might simply add a method that returns each result as an [out] parameter. Client programmers working in C#, for instance, would be left to contend with a method signature such as the following:

C++
void MeasureIQ(out string DataType, out int SpreadFactor, out int CodeNumber, out double Amplitude);

Designing methods with multiple output parameters is generally bad form. Client applications are forced to set up multiple local variables just to retrieve the data (even if they are only interested in one piece of data). In addition, if an instrument happens to return a series of related results, then each of the [out] parameters above would be arrays of results and the client code would be even more cumbersome.

A better design for methods that return multiple results would be to consolidate all of the result items into a single results object and provide that object as the method's return value. In the C programming language, the struct data type would work nicely. In COM, we can model a struct by creating an interface with read-only properties that return each of the result items. For the example above, the interface that we would need would look like the following to a C# client:

C++
interface IAcme4321IQResults
{
   string DataType { get; }     // NOTE: Each property is read-only
   int Spreadfactor { get; }
   int CodeNumber { get; }
   double Amplitude { get; }
}

With the IAcme4321IQResults interface in hand, we can now define a much more client-friendly MeasureIQ method:

C++
IAcme4321IQResults MeasureIQ();
Adding an Object Parameter to a Driver Method

Adding an object parameter to a method is considerably more complex than adding simple data type parameters. The process consists of the following steps, described in detail in this topic:

  1. Adding an interface that contains the result items.

  2. Adding a COM coclass to implement the results interface.

  3. Adding a C++ class to implement the COM coclass.

  4. Coding the driver method to return an instance of the COM class.

  5. Updating the driver installer to register the COM class.

Step 1 -- Adding an interface that contains the result items

The first step in adding a method that returns multiple results as a single unit, is to add an interface that contains each of the results. Note that since this interface will be implemented on a non-Nimbus-managed class, it must be added as a "free-standing" interface from the IVI-COM Designer flat tree view (not from the hierarchical tree view).

To add an interface for returning multiple method results

  1. From the IVI-COM Designer, right click on the Interfaces and choose Add Interface.

  2. The interface node enters edit mode. Enter the desired name of the interface.

  3. The interface is added to the driver project.

  4. From the flat tree view of the IVI-COM Designer, add the following four properties of the indicated type:

    • DataType : String

    • Spreadfactor : int

    • CodeNumber : int

    • Amplitude : double

  5. Change each of the above properties to be read-only.

Step 2 -- Adding a COM coclass to implement the results interface

COM interfaces returned from methods must be implemented by a special COM class, known as a coclass. The coclass for our results object must be defined in IDL. Since Nimbus automatically manages the IDL file for driver projects, special steps must be performed to "inject" custom IDL into the driver to support our custom results object.

To add a COM coclass that implements the results interface

  1. Add the following IDL files to your driver project:

    • CustomClasses.idl

    • CustomEnums.idl

    • CustomInterfaceDeclarations.idl

    • CustomInterfaceDefinitions.idl

    Each file can be added to your driver project from Solution Explorer by right-clicking the Source Files folder and choosing Add and then selecting New Item.... The Add New Item Dialog Box appears.

    From the tree view on the left, choose Code and then select Midl File (.idl) from the file types on the left.

    Enter the appropriate file name (CustomClasses, for example) and click Add. An empty IDL file is added to the driver project.

  2. From Solution Explorer, select all four of the above files and choose Properties. The Properties Page Dialog for the files appears.

    From the Configuration combo box, choose All Configurations.

    From the Platform combo box, choose All Platforms.

    From the tree view on the left, choose the General page.

    Set Excluded From Build to Yes.

  3. Copy the following code into CustomClasses.idl.

    C++
    [
        uuid(COCLASSG-UIDC-OCLA-SSGU-IDCOCLASSGUI),
        helpstring(""),
        helpcontext(0)
    ]
    coclass Acme4321IQResults
    {
        [default] interface IAcme4321IQResults;
    };
  4. The uuid attribute in the above code needs to have a unique value. Unique GUIDs can be generated from within Visual Studio by choosing Create GUID from the Tools. Replace the uuid attribute value COCLASSG-UIDC-OCLA-SSGU-IDCOCLASSGUI with a newly generated GUID.

  5. The other three IDL files added previously are generally only needed in advanced scenarios. In most instances, they can remain empty (though they must be present in the project for the driver to compile).

  6. In order to instruct Nimbus to merge the contents of the above IDL files with the Nimbus managed driver IDL file, a special MIDL preprocessor symbol must be defined.

    From Solution Explorer, right-click on the driver project, and choose Properties.

    From the Configuration combo box, choose All Configurations.

    From the Platform combo box, choose All Platforms.

    From the tree view on the left, choose the MIDL page.

    Add CUSTOM_IDL to the list of Preprocessor Definitions.

  7. The COM class that will hold our results needs to be registered with COM. This is done via a registration script (.rgs) file.

    From Solution Explorer, right-click on the Resources Files folder and choose Add New Item.... The Add New Item Dialog Box appears.

  8. From the tree view on the left, choose Resource and then select Registration Script (.rgs) from the file types on the right.

  9. Enter the name CoAcme4321IQResults and click Add. The file CoAcme4321IQResults.rgs is added to the driver project.

  10. Copy the following code into the CoAcme4321IQResults.rgs file.

    C++
    HKCR
    {
        Acme4321.Acme4321IQResults.1 = s 'Acme4321IQResults Class'
        {
            CLSID = s '{COCLASSG-UIDC-OCLA-SSGU-IDCOCLASSGUI}'
        }
        Acme4321.Acme4321IQResults = s 'Acme4321IQResults Class'
        {
            CLSID = s '{COCLASSG-UIDC-OCLA-SSGU-IDCOCLASSGUI}'
            CurVer = s 'Acme4321.Acme4321IQResults.1'
        }
        NoRemove CLSID
        {
            ForceRemove {COCLASSG-UIDC-OCLA-SSGU-IDCOCLASSGUI} = s 'Acme4321IQResults Class'
            {
                ProgID = s 'Acme4321.Acme4321IQResults.1'
                VersionIndependentProgID = s 'Acme4321.Acme4321IQResults'
                InprocServer32 = s '%MODULE%'
                {
                    val ThreadingModel = s 'Both'
                }
                val AppID = s '%APPID%'
                'TypeLib' = s '{LIBIDLIB-IDLI-BIDL-IBID-LIBIDLIBIDLI}'
            }
        }
    }

    Replace the {COCLASSG-UIDC-OCLA-SSGU-IDCOCLASSGUI} with the same GUID you assigned to the coclass definition in the CustomClasses.idl file value. Unique GUIDs can be generated from within Visual Studio by choosing Create GUID from the Tools.

    Replace the {LIBIDLIB-IDLI-BIDL-IBID-LIBIDLIBIDLI} with the driver's type library GUID (referred to as the LIBID). The LIBID can be found in the Nimbus-generated IDL file -- Acme4321.nimbus.idl. The LIBID is the value of the uuid attribute assigned to the library block, as shown in the following IDL code snippet:

    C++
    #ifdef CUSTOM_IDL
    #include "CustomInterfaceDeclarations.idl"
    #endif
    
    [
        uuid(LIBIDLIB-IDLI-BIDL-IBID-LIBIDLIBIDLI),
        version(0.1),
        helpstring(""),
        helpcontext(0)
    ]
    library Acme4321
    {
        importlib("stdole2.tlb");
        importlib("IviDriverTypeLib.dll");
  11. Add the following line of code to the driver project's Resource.h file, using a value of at least 200.

    C++
    #define IDR_COACME4321IQRESULTS    201
  12. Right click on the driver project's .rc file (Acme4321.rc) and choose View Code. Add the following line of code to the bottom of the file:

    C++
    IDR_COACME4321IQRESULTS          REGISTRY               "CoAcme4321IQResults.rgs"

Step 3 -- Adding a C++ class to implement the COM coclass

The COM coclass defined in the previous steps exists mostly to register the COM object. The code that will implement the methods and properties of our IAcme4321IQResults interface will be supplied by a C++ class.

To add a C++ class for implementing the COM coclass

  1. From Solution Explorer, right-click on the driver project and choose Add and then select New Item.... The Add New Item Dialog Box appears.

  2. From the tree view on the left, choose Code and then select Header File (.h) from the file types on the left.

  3. Enter a file name of CoAcme4321IQResults and click Add. The file CoAcme4321IQResults.h is added to the driver project.

  4. Replace the contents of the CoAcme4321IQResults.h file with the following code:

    C++
    #pragma once
    #include "resource.h"
    #include "_Acme4321.h"
    
    class ATL_NO_VTABLE Acme4321IQResults :
        public CComObjectRootEx<CComMultiThreadModel>,
        public CComCoClass<Acme4321IQResults, &__uuidof(Acme4321IQResults)>,
        public IAcme4321IQResults
    {
    public:
    DECLARE_REGISTRY_RESOURCEID(IDR_ACME4321IQDATA)
    
    BEGIN_COM_MAP(Acme4321IQResults)
        COM_INTERFACE_ENTRY(IAcme4321IQResults)
    END_COM_MAP()
    
        DECLARE_PROTECT_FINAL_CONSTRUCT()
    
    private:
       CString m_strDataType;
       long m_lSpreadFactor;
       long m_lCodeNumber;
       double m_dChannelAmplitude; 
    
    public:
       void Configure(const CString& strDataType, long lSpreadFactor, long lCodeNumber, double dChannelAmplitude);
    
    public:
        STDMETHOD(get_DataType)(BSTR* val);
        STDMETHOD(get_SpreadFactor)(long* val);
        STDMETHOD(get_CodeNumber)(long* val);
        STDMETHOD(get_ChannelAmplitude)(double* val);
    };
    
    OBJECT_ENTRY_AUTO(__uuidof(Acme4321IQResults), Acme4321IQResults)
  5. From Solution Explorer, right-click on the driver project and choose Add and then select New Item.... The Add New Item Dialog Box appears.

  6. From the tree view on the left, choose Code and then select C++ File (.cpp) from the file types on the left.

  7. Enter a file name of CoAcme4321IQResults and click Add. The file CoAcme4321IQResults.cpp is added to the driver project.

  8. Replace the contents of the CoAcme4321IQResults.cpp file with the following code:

    C++
    #include "stdafx.h"
    #include "CoAcme4321IQResults.h"
    
    void Acme4321IQResults::Configure(const CString& strDataType, long lSpreadFactor, long lCodeNumber, double dChannelAmplitude)
    {
       m_strDataType = strDataType;
       m_lSpreadFactor = lSpreadFactor;
       m_lCodeNumber = lCodeNumber;
       m_dChannelAmplitude = dChannelAmplitude;
    }
    
    STDMETHODIMP Acme4321IQResults::get_DataType(BSTR* val)
    {
       if (val == NULL)
          return E_POINTER;
    
       *val = m_strDataType.AllocSysString();
    
       return S_OK;
    }
    
    STDMETHODIMP Acme4321IQResults::get_SpreadFactor(long* val)
    {
       if (val == NULL)
          return E_POINTER;
    
       *val = m_lSpreadFactor;
    
       return S_OK;
    }
    
    STDMETHODIMP Acme4321IQResults::get_CodeNumber(long* val)
    {
       if (val == NULL)
          return E_POINTER;
    
       *val = m_lCodeNumber;
    
       return S_OK;
    }
    
    STDMETHODIMP Acme4321IQResults::get_ChannelAmplitude(double* val)
    {
       if (val == NULL)
          return E_POINTER;
    
       *val = m_dChannelAmplitude;
    
       return S_OK;
    }

Step 4 -- Coding the driver method to return an instance of the COM class

Once the COM class for the results object has been defined and implemented, the next step is to implement the driver method that packages up the results of the method and returns an instance of the class. Following with the current example, the MeasureIQ method below demonstrates the code necessary to return a results object from a driver method.

C++
// CoAcme4321.cpp

HRESULT Acme4321::MeasureIQ(IAcme4321IQResults** ppResults)
{
   HRESULT hr = S_OK;

   // Send a query command to the instrument.  Response is a comma-separated list of four values.
   CString strDataType;
   long lSpreadFactor;
   long lCodeNumber;
   double dChannelAmplitude;
   hr = io.Queryf(_T("SENS:IQ:DATA?"), _T("%$Cs,%d,%d,%le"), &strDataType, &lSpreadFactor,  &lCodeNumber, &dChannelAmplitude);
   if (SUCCEEDED(hr))
   {      
      // Create an instance of the Acme4321IQResults COM class to hold the method results
      CComObject<Acme4321IQResults>* pResults = NULL;
      hr = CComObject<Acme4321IQResults>::CreateInstance(&pResults);
      if (SUCCEEDED(hr))
      {
         // Initialize the properties on the results object
         pResults->Configure(strDataType, lSpreadFactor, lCodeNumber, dChannelAmplitude); 

         // Get the right interface pointer type
         pResults->QueryInterface(ppResults);
      }
   }

   return hr;
}

Step 5 -- Updating the driver installer to register the COM class

The code above to create an instances of our Acme4321IQResults class will only work if the COM class is properly registered. On a Nimbus developer machine, this registration happens automatically as part of the build. But, when the driver is deployed to an end-user machine, then the registration for this class must be performed by the driver installer. For the main driver class and repeated capability classes, Nimbus automatically updates the driver installer to create the appropriate entries. Also, since the IAcme4321IQResults interface was added via the Nimbus designer, Nimbus automatically updates the installer to register this interface on the end-user machine. However, the COM class definition was added manually, so the installer must be manually updated to register the COM class on the end-user machine during the driver installation process.

To add a COM class registration to the driver installer:

  1. From Solution Explorer, right-click on the Setup.wxs file in the driver setup project, and choose View Code.

  2. Locate the two Class elements for the main driver class. There will be two elements -- one in the 32-bit section of the Setup.wxs file and one in the 64-bit section of the Setup.wxs file. The existing entry for the main driver class will look something like the following:

    XML
     <!-- Setup.wxs -->
    
    <Class Id="{XXXXXXXG-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" Advertise="no" Context="InprocServer32" Description="Acme4321 Object" ... >
       <ProgId Id="Acme4321.Acme4321.1">
          <ProgId Id="Acme4321.Acme4321" />
       </ProgId>
    </Class>
  3. Copy the Class element for the main driver class and paste it just beneath the existing entry. Update the Id and Description attributes on the Class element as well as the Id attributes on the ProgId elements. The values must match those used in the .rgs and .idl files in previous steps. The result should look something like the following:

    XML
     <!-- Setup.wxs -->
    
    <Class Id="{XXXXXXXG-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" Advertise="no" Description="Acme4321 Object" Context="InprocServer32" ... >
       <ProgId Id="Acme4321.Acme4321.1">
          <ProgId Id="Acme4321.Acme4321" />
       </ProgId>
    </Class>
    <Class Id="{COCLASSG-UIDC-OCLA-SSGU-IDCOCLASSGUI}" Advertise="no" Description="Acme4321IQResults Object" Context="InprocServer32" ... >
       <ProgId Id="Acme4321.Acme4321IQResults.1">
          <ProgId Id="Acme4321.Acme4321IQResults" />
       </ProgId>
    </Class>
  4. Repeat the above process for the Class element in the 32-bit and 64-bit sections of the Setup.wxs file.

Using Arrays of Object Parameters in Driver Methods

Some driver methods may need to return an array of object parameters. The process for doing this is very similar to the process above for returning a single object parameter. The only difference is in the final step -- the implementation of the driver method itself.

With the COM class defined and implemented according to the steps above, the driver method for returning an array of our example Acme4321IQResults object would look like the following:

C++
// CoAcme4321.cpp

HRESULT Acme4321::MeasureIQMultiple(SAFEARRAY** ppsaResults)
{
   HRESULT hr = S_OK;

   // Send a query command to the instrument
   CString strResponse;
   hr = io.Queryf(_T("SENS:IQ:DATA:ALL?"), _T("%$Cs"), strResponse);
   if (SUCCEEDED(hr))
   {
      CAtlArray<CString> rgstrDataType;
      CAtlArray<long> rglSpreadFactor;
      CAtlArray<long> rglCodeNumber;
      CAtlArray<double> rgdChannelAmplitude;

      int nNumResults = (int)rgstrDataType.GetCount();

      // Parse the strResponse string into the local variables above.
      // This code is not shown for clarity.
      // ...

      // Declare an array of objects to store the results
      CComSafeArray<IUnknown*> saResults;

      // Create the correct number of elements in the results array
      saResults.Create(nNumResults);

      for (int i = 0; i < nNumResults && SUCCEEDED(hr); i++)
      {
         // Create a single results object
         CComObject<Acme4321IQResults>* pResult = NULL;
         hr = CComObject<Acme4321IQResults>::CreateInstance(&pResult);
         if (SUCCEEDED(hr))
         {
            CString strDataType = rgstrDataType.GetAt(i);
            long lSpreadFactor = rglSpreadFactor.GetAt(i);
            long lCodeNumber = rglCodeNumber.GetAt(i);
            double dChannelAmplitude = rgdChannelAmplitude.GetAt(i);

            // Initialize the properties on the results object
            pResult->Configure(strDataType, lSpreadFactor, lCodeNumber, dChannelAmplitude); 

            // Get right interface pointer type
            IUnknown* pUnk = NULL;
            pResult->QueryInterface(&pUnk);

            // Add the result object to the array of result objects
            saResults.SetAt(i, pUnk);
         }
      }

      if (SUCCEEDED(hr))
      {
         *ppsaResults = saResults.Detach();
      }
   }

   return hr;
}

Download a complete CHM version of this documentation here.