views:

65

answers:

2

My goal is to display a .NET Windows forms message box from a pure C++ native Windows API-level program (not managed C++ or C++/CLI).

That is, for learning purposes I want to implementing the C# code shown in the comment below, in pure C++:

/*
    // C# code that this C++ program should implement:
    using System.Windows.Forms;

    namespace hello
    {
        class Startup
        {
            static void Main( string[] args )
            {
                MessageBox.Show(
                    "Hello, world!",
                    ".NET app:",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Information
                    );
            }
        }
    }
*/

#include <stdexcept>
#include <string>
#include <iostream>
#include <stdlib.h>         // EXIT_SUCCESS, EXIT_FAILURE

#undef  UNICODE
#define UNICODE
#include <windows.h>

#include <Mscoree.h>
#include <comdef.h>
_COM_SMARTPTR_TYPEDEF( ICorRuntimeHost, IID_ICorRuntimeHost );      // ICorRuntimeHostPtr

// #import is an MS extension, generates a header file. Will be replaced with #include.
#import "C:\\WINDOWS\\Microsoft.NET\\Framework\\v1.1.4322\\mscorlib.tlb" \
    raw_interfaces_only rename( "ReportEvent", "reportEvent" )
typedef mscorlib::_AppDomainPtr     AppDomainPtr;
typedef mscorlib::_ObjectHandlePtr  ObjectHandlePtr;
typedef mscorlib::_AssemblyPtr      AssemblyPtr;

bool throwX( std::string const& s ) { throw std::runtime_error( s ); }

template< class Predicate >
struct Is: Predicate
{};

template< class Type, class Predicate >
bool operator>>( Type const& v, Is< Predicate > const& check )
{
    return check( v );
}

struct HrSuccess
{
    bool operator()( HRESULT hr ) const
    {
        ::SetLastError( hr );
        return SUCCEEDED( hr );
    }
};

void cppMain()
{
    ICorRuntimeHostPtr  pCorRuntimeHost;
    CorBindToRuntimeEx(
        L"v1.1.4322",           // LPWSTR pwszVersion,  // RELEVANT .NET VERSION.
        L"wks",                 // LPWSTR pwszBuildFlavor, // "wks" or "svr"
        0,                      // DWORD flags,
        CLSID_CorRuntimeHost,   // REFCLSID rclsid,
        IID_ICorRuntimeHost,    // REFIID riid,
        reinterpret_cast<void**>( &pCorRuntimeHost )
        )
        >> Is< HrSuccess >()
        || throwX( "CorBindToRuntimeEx failed" );

    pCorRuntimeHost->Start()    // Without this GetDefaultDomain fails.
        >> Is< HrSuccess >()
        || throwX( "CorRuntimeHost::Start failed" );

    IUnknownPtr     pAppDomainIUnknown;
    pCorRuntimeHost->GetDefaultDomain( &pAppDomainIUnknown )
        >> Is< HrSuccess >()
        || throwX( "CorRuntimeHost::GetDefaultDomain failed" );

    AppDomainPtr    pAppDomain  = pAppDomainIUnknown;
    (pAppDomain != 0)
        || throwX( "Obtaining _AppDomain interface failed" );

    // This fails because Load requires a fully qualified assembly name.
    // I want to load the assembly given only name below + relevant .NET version.
    AssemblyPtr     pFormsAssembly;
    pAppDomain->Load_2( _bstr_t( "System.Windows.Forms" ), &pFormsAssembly )
        >> Is< HrSuccess >()
        || throwX( "Loading System.Windows.Forms assembly failed" );    

    // ... more code here, not yet written.
}

int main()
{
    try
    {
        cppMain();
        return EXIT_SUCCESS;
    }
    catch( std::exception const& x )
    {
        std::cerr << "!" << x.what() << std::endl;
    }
    return EXIT_FAILURE;
}

The plan is, having loaded the assembly, proceed to MessageBox class and invoke Show. But this just may be a mistaken way of doing it. So I'm equally happy with an answer showing how to do that without finding the fully qualified name of the assembly (of course, without hardcoding that fully qualified name!).

The gacutil utility is evidently able to find fully qualified names:

C:\test> gacutil /l System.Windows.Forms
Microsoft (R) .NET Global Assembly Cache Utility.  Version 4.0.30319.1
Copyright (c) Microsoft Corporation.  All rights reserved.

The Global Assembly Cache contains the following assemblies:
  System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL
  System.Windows.Forms, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL

Number of items = 4

C:\test> _

However, as mentioned I don't want to hardcode anything: the .NET info hardcoded in the C++ code should not be more than in the C# source code shown in the comment at the top, plus the minimum .NET version supported.

TIA.,

+1  A: 

Hmya, this is not the way it is normally done. After initializing the CLR, you'd next load an assembly that is not in the GAC, produced by a managed compiler. Typically an EXE with a Main() method but you've got alternate options here of course.

If you want to pursue this then you really do have to produce a fully qualified name, Fusion requires it to know which assembly to pick from the GAC. The equivalent of gacutil /l is the Fusion API, you'd use CreateAssemblyEnum() to iterate it. Gets you an IAssemblyEnum, its GetNextAssembly() method gets you an IAssemblyName. There has to be some hard-coded selection algorithm to decide exactly which version you'd prefer.

This is not a problem when you use an assembly created by a managed compiler. The assembly references in the project select the desired version. I'd have to recommend you head that way.

Hans Passant
Thanks, so far. Essentially (unless other way of achieving higher level goal of getting to `MessageBox` class is found) I probably want to do what that compiler does for assembly selection. If as you say it just picks from project assembly references then I want the selection that was done to produce those assembly references. Cheers,
Alf P. Steinbach
The compiler doesn't do it either. The programmer does when he creates his project. Or perhaps more accurately, the project template does. This stuff is built into the IDE. There's no 'selection' mechanism the way you are approaching it now.
Hans Passant
Thanks again, so far. But really, we old dinosaurs don't use them fancy IDEs. :-) Keyboard! Or, for the real top dinos, altering the bits in RAM by directing cosmic rays via magic incantantions! Cheers,
Alf P. Steinbach
+1  A: 

Hm, OK, partial answer to my own question (enough to go forward):

I supply a constructed full-name with the version set as the minimum version of .NET supported, and the .NET "Fusion" (but is this documented?) finds an apparently compatible assembly, although the specified assembly is not in the GAC:

AssemblyPtr     pFormsAssembly;
pAppDomain->Load_2( _bstr_t( "System.Windows.Forms, Version=1.1.4322.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" ), &pFormsAssembly )
    >> Is< HrSuccess >()
    || throwX( "Loading System.Windows.Forms assembly failed" );
std::cout << "Loaded the assembly." << std::endl;

_bstr_t     displayName;
pFormsAssembly->get_ToString( displayName.GetAddress() )
    >> Is< HrSuccess >()
    || throwX( "_Assembly::get_ToString failed" );
std::cout << "\"" << displayName.operator char const*() << "\"" << std::endl;

with output

Loaded the assembly.
"System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"

I think the UUID (here b77a5c561934e089) is just part of the general version-independent identity of the assembly, but ideally I'd pick up also that dynamically in the code.

After all, it doesn't need to be specified in the C# source code -- can it be done?

Cheers,

Alf P. Steinbach