Interop with .NET from FoxPro can be accomplished fairly easily by using COM Interop. With COM Interop you can take advantage of .NET's COM Callable Wrapper to wrap up .NET components and make them available as COM objects. I've written extensively about this interaction (here and here) and while this process works reasonably well it has one very annoying problem: COM Registration requirements.
If you want to use .NET Components through COM interop you have to specifically access objects that are visible to COM and are registered on the machine. Since .NET internally has rather few components that are exposed as COM objects and most components and libraries don't expose to COM directly, if you ever want to interoperate with these types of components you'll always have to create a wrapper component first in .NET that can act as a proxy between your unmanaged application the managed component. Often this can be a good thing - giving you a much cleaner interface, but at other times it's also just just overkill and quickly results in a conglomeration of excessive number of shim classes.
In addition, COM Registration of .NET Components is also a bit complicated in that .NET Components require a special utility RegAsm to register rather than just the standard regsvr32 so getting components registered and updated can be a pain. It's not exactly what you'd call xCopy deployment if COM registration is involved.
Avoiding COM Instantiation - Hosting the CLR directly
So... wouldn't it be cool if you could instead load any .NET component (ok, not *any and all* but not just COM registered ones) and without having to register a custom component on the machine first? In other words - simply be able to access a .NET directly from an unmanaged application rather than explicitly requiring COM ids?
It turns out this is quite easy if you host the .NET runtime directly in your unmanaged application. So rather than having to instantiate an object through COM and a ProgId/ClassId you can instantiate the object through the .NET CLR Hosting mechanism and then pass back an IDispatch (or IUnknown) pointer back to the host application.
To do this requires some low level code and the easiest way to do this is with a small shim C++ DLL. The DLL will provide a couple of simple exported functions that allow loading of the CLR and instantiating a type which is then passed back as an IDispatch pointer to VFP. Surprisingly creating this code in C++ is pretty simple. Here's a single source file that exposes standard Win32 functions callable directly from VFP that does the trick:
#include "stdafx.h"
#include <stdio.h>
#include <atlbase.h>
#include <mscoree.h>
#import "C:\\Windows\\Microsoft.NET\\Framework\\v2.0.50727\\Mscorlib.tlb" raw_interfaces_only
using namespace mscorlib;
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
return TRUE;
}
/// Globals - assume single instance for this DLL
CComPtr<ICorRuntimeHost> spRuntimeHost = NULL;
CComPtr<_AppDomain> spDefAppDomain = NULL;
/// Assigns an error message
DWORD WINAPI SetError(HRESULT hr, char *ErrorMessage)
{
if (ErrorMessage)
{
int len= strlen(ErrorMessage);
LoadStringRC(hr,(LPWSTR)ErrorMessage,len/2,0);
sprintf((char *)ErrorMessage,"%ws",ErrorMessage);
return strlen(ErrorMessage);
}
strcpy(ErrorMessage,"");
return 0;
}
/// Starts up the CLR and creates a Default AppDomain
DWORD WINAPI ClrLoad(char *ErrorMessage, DWORD *dwErrorSize)
{
if (spDefAppDomain)
return 1;
//Retrieve a pointer to the ICorRuntimeHost interface
HRESULT hr = CorBindToRuntimeEx(
NULL, //Retrieve latest version by default
L"wks", //Request a WorkStation build of the CLR
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,
CLSID_CorRuntimeHost,
IID_ICorRuntimeHost,
(void**)&spRuntimeHost
);
if (FAILED(hr))
{
*dwErrorSize = SetError(hr,ErrorMessage);
return hr;
}
//Start the CLR
hr = spRuntimeHost->Start();
if (FAILED(hr))
return hr;
CComPtr<IUnknown> pUnk;
//Retrieve the IUnknown default AppDomain
hr = spRuntimeHost->GetDefaultDomain(&pUnk);
if (FAILED(hr))
return hr;
hr = pUnk->QueryInterface(&spDefAppDomain.p);
if (FAILED(hr))
return hr;
return 1;
}
// *** Unloads the CLR from the process
DWORD WINAPI ClrUnload()
{
if (spDefAppDomain)
{
spRuntimeHost->UnloadDomain(spDefAppDomain.p);
spDefAppDomain.Release();
spDefAppDomain = NULL;
spRuntimeHost->Stop();
spRuntimeHost.Release();
spRuntimeHost = NULL;
}
return 1;
}
// *** Creates an instance by Name (ie. local path assembly without extension or GAC'd FullName of
// any signed assemblies.
DWORD WINAPI ClrCreateInstance( char *AssemblyName, char *className, char *ErrorMessage, DWORD *dwErrorSize)
{
CComPtr<_ObjectHandle> spObjectHandle;
if (!spDefAppDomain)
{
if (ClrLoad(ErrorMessage,dwErrorSize) != 1)
return -1;
}
DWORD hr;
//Creates an instance of the type specified in the Assembly
hr = spDefAppDomain->CreateInstance(
_bstr_t(AssemblyName),
_bstr_t(className),
&spObjectHandle
);
*dwErrorSize = 0;
if (FAILED(hr))
{
*dwErrorSize = SetError(hr,ErrorMessage);
return -1;
}
CComVariant VntUnwrapped;
hr = spObjectHandle->Unwrap(&VntUnwrapped);
if (FAILED(hr))
return -1;
CComPtr<IDispatch> pDisp;
pDisp = VntUnwrapped.pdispVal;
return (DWORD) pDisp.p;
}
/// *** Creates an instance of a class from an assembly referenced through it's disk path
DWORD WINAPI ClrCreateInstanceFrom( char *AssemblyFileName, char *className, char *ErrorMessage, DWORD *dwErrorSize)
{
CComPtr<_ObjectHandle> spObjectHandle;
if (!spDefAppDomain)
{
if (ClrLoad(ErrorMessage,dwErrorSize) != 1)
return -1;
}
DWORD hr;
//Creates an instance of the type specified in the Assembly
hr = spDefAppDomain->CreateInstanceFrom(
_bstr_t(AssemblyFileName),
_bstr_t(className),
&spObjectHandle
);
*dwErrorSize = 0;
if (FAILED(hr))
{
*dwErrorSize = SetError(hr,ErrorMessage);
return -1;
}
CComVariant VntUnwrapped;
hr = spObjectHandle->Unwrap(&VntUnwrapped);
if (FAILED(hr))
return -1;
CComPtr<IDispatch> pDisp;
pDisp = VntUnwrapped.pdispVal;
// *** pass the raw COM pointer back
return (DWORD) pDisp.p;
}
Not a lot of code here - this library exposes 4 public methods through its DLL interface that are callable directly through Win32 DLL semantics. In FoxPro this means these functions are accessible with DECLARE DLL.
The first two methods are to load and unload the CLR. The load routine bootstraps the CLR and then loads a default AppDomain into which any types will be loaded later. The references to the Host and the AppDomain are global references that stick around - there's an assumption that there's only one AppDomain loaded and managed through this particular DLL which should be fine for a VFP application, but you can call ClrUnload to release the AppDomain and shut down the CLR (although it will not actually completely shut down this way - once the loaded the CLR never completely unloads). Shutting down an AppDomain is a good way to release memory of all the components hosted and assemblies hosted within it.
The key worker methods are the ClrCreateInstance and ClrCreateInstanceFrom methods which are the main external interface and can be used alone to to bootstrap the CLR if necessary and then hand back an object reference to the type specified to the client.
These function load a type into the .NET AppDomain and return it to the host via an IDispatch pointer. CreateInstance works of an assembly name - which is either a local file (without extension) in application's root folder, or a fully qualified assembly name (ie. System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089) and the name of a type - like System.Net.Mail.MailMessage for example. CreateInstanceFrom uses a fully qualified path to the assembly instead and allows loading from arbitrary locations on disk given appropriate permissions and trust settings for the application.
These functions basically use the AppDomain COM reference to load up a new instance of the type. They then turn that return type into a variant and cast it as an IDispatch pointer that is then passed back to the caller. The IDispatch pointer is returned cast as an unsigned integer so it can be easily returned to Visual FoxPro.
In FoxPro then the SYS(3096) function can be used to take a numeric pointer value and can turn it back into an IDispatch pointer. The pointer then acts just as you would expect a COM object to behave and voila you now have access to a .NET object. Here are the two wrappers, both of which return a pointer to the created type (or NULL on failure):
************************************************************************
* CreateInstance
****************************************
*** Function: Creates an instance of a .NET class by full assembly
*** name or local file name.
*** Assume:
*** Pass: lcLibrary - either one of these formats
*** System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
*** MyAssembly (where the assembly file is in the App's start path)***
*** lcClass - MyNamespace.MyClass
*** Return: Instance or NULL
************************************************************************
FUNCTION CreateClrInstance(lcLibrary,lcClass,lcError)
LOCAL lnDispHandle, lnSize
DECLARE Integer ClrCreateInstance IN ClrHost.dll string, string, string@, integer@
lcError = SPACE(2048)
lnSize = 0
lnDispHandle = ClrCreateInstance(lcLibrary,lcClass,@lcError,@lnSize)
IF lnDispHandle < 1
lcError = LEFT(lcError,lnSize)
RETURN NULL
ENDIF
RETURN SYS(3096, lnDispHandle)
ENDFUNC
************************************************************************
* CreateClrInstanceFrom
****************************************
*** Function: Creates an instance of a .NET class by referencing
*** a fully qualified assembly path.
*** Pass: lcLibrary - fully qualified path to the assembly
*** including extension
*** lcClass - MyNamespace.MyClass
*** Return: Instance or NULL
************************************************************************
FUNCTION CreateClrInstanceFrom(lcLibrary,lcClass,lcError)
LOCAL lnDispHandle, lnSize
DECLARE Integer ClrCreateInstanceFrom IN ClrHost.dll string, string, string@, integer@
lcError = SPACE(2048)
lnSize = 0
lnDispHandle = ClrCreateInstanceFrom(lcLibrary,lcClass,@lcError,@lnSize)
IF lnDispHandle < 1
lcError = LEFT(lcError,lnSize)
RETURN NULL
ENDIF
RETURN SYS(3096, lnDispHandle)
ENDFUNC
With this code in place you can now start instantiating types from the .NET runtime or your own assemblies. You're still dealing with COM and all the COM related issues when making these calls, but there is no need to instantiate COM registered components by the ClassId/ProgId. In other words using this approach you can load and access most .NET objects DIRECTLY.
Here's a very simple example sending email trough the new System.Net mail client in .NET 2.0 using these wrappers:
CLEAR
lcSysDll = "System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
lcError = ""
loMsg = CreateInstance(lcSysDLL,"System.Net.Mail.MailMessage",@lcError)
loSmtp = CreateInstance(lcSysDLL,"System.Net.Mail.SmtpClient",@lcError)
IF ISNULL(loMsg) OR ISNULL(loSMTP)
? lcError
RETURN
ENDIF
*** Show Class Names from .NET
? loMsg.ToString()
? loSMTP.ToString()
loSmtp.Host = "mail.frogbyte.net"
loSmtp.Send("lola@run-run.com","morris@softunderbelly.com","Hello World","Body this, body that")
Now remember this happens now without explicit COM registration - you are simply calling the DLL to host the CLR in VFP and then make calls to the returned instances. All the interaction still occurs over COM boundaries, but the requirement for a ProgId or ClassId for invokation of the object has been removed.
Notice that I set lcSysDll to the full .NET assembly name which is required for any signed component and any component in the Global Assembly Cache. If you're new to .NET you may be wondering where to find these Ids. I highly recommend you immediately download Reflector which is an assembly browsing utility. One of the things it does is provide detailed information about each assembly including the assembly name. Reflector will also be VERY useful as you use .NET components, as it shows you the classes and members available in the library as well as a disassembler that can even show you how the code works. If you're using .NET in any way Reflector is the first tool you need to have!
What these wrapper functions mean is that you can create objects from the .NET Base Class Library (BCL) directly, but more importantly you can also create a new .NET DLL and access it without having to register it for COM interop - no more polluting of the registry with gazillions of COM interfaces you need to expose just to get access to a few method calls.
More Flexibility - Creating one more wrapper
There are a few problems with the code above. First the error reporting from DLL code doesn't provide clean messages which is somewhat important for the actual Assembly loading code. There's a function LoadStringsRC() that is supposed to translate error information properly, but I couldn't get this to return proper errors. Error reporting is pretty important though - if an assembly or type fails to load you certainly will want to know why.
Another issue is that - as written - the C++ code can only instantiate objects that have a parameterless constructor. Any object that requires parameters in its constructor cannot be instantiated through this mechanism. Surprisingly there are quite a lot of objects in .NET that fall into this category. While it's possible to fix this in the call to CreateInstance and CreateInstanceFrom calls in the C++ code it's quite a bit of work to make this happen with SAFEARRAYS in C++ (not my forte <s>).
So another approach that is possibly more easily extended and also has the benefit of providing much richer error information is to use another proxy loader in the form a generic .NET class that acts as a loader. So in this scenario you use the C++ DLL code to load the .NET Bridge Loader and then hang on to this loader to load any other objects.
The advantage here is that it's much easier to create this client code for various constructor parameter combinations and it can also serve as a utility function container. Errors are captured inside of the .NET code so you can get more complete error information on the instantiation.
The wrapper is again very simple. Here it is:
using System;
using System.Runtime.InteropServices;
using System.Reflection;
namespace Westwind.WebConnection
{
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
public class wwDotNetBridge
{
/// <summary>
/// Returns error information if the call fails
/// </summary>
public string ErrorMessage
{
get { return _ErrorMessage; }
set { _ErrorMessage = value; }
}
private string _ErrorMessage = "";
/// <summary>
/// Creates an instance from a file reference with a parameterless constructor
/// </summary>
/// <param name="AssemblyFileName"></param>
/// <param name="TypeName"></param>
/// <returns></returns>
public object CreateInstanceFromFile(string AssemblyFileName, string TypeName)
{
return this.CreateInstanceFromFile_Internal(AssemblyFileName, TypeName, null);
}
/// <summary>
/// Creates an instance from a file reference with a 1 parameter constructor
/// </summary>
/// <param name="AssemblyFileName"></param>
/// <param name="TypeName"></param>
/// <returns></returns>
public object CreateInstanceFromFile_OneParm(string AssemblyFileName, string TypeName, object Parm1)
{
return this.CreateInstanceFromFile_Internal(AssemblyFileName, TypeName, new Object[1] { Parm1 } );
}
/// <summary>
/// Creates an instance from a file reference with a two parameter constructor
/// </summary>
/// <param name="AssemblyFileName"></param>
/// <param name="TypeName"></param>
/// <returns></returns>
public object CreateInstanceFromFile_TwoParms(string AssemblyFileName, string TypeName, object Parm1, object Parm2)
{
return this.CreateInstanceFromFile_Internal(AssemblyFileName, TypeName, new Object[2] { Parm1 , Parm2} );
}
/// <summary>
/// Creates a new instance from a file file based assembly refence. Requires full
/// filename including extension and path.
/// </summary>
/// <param name="AssemblyFileName"></param>
/// <param name="TypeName"></param>
/// <returns></returns>
protected object CreateInstanceFromFile_Internal(string AssemblyFileName, string TypeName)
{
return this.CreateInstance_Internal(AssemblyFileName, TypeName, null);
}
/// <summary>
/// Creates a new instance from a file file based assembly refence. Requires full
/// filename including extension and path.
/// </summary>
/// <param name="AssemblyFileName"></param>
/// <param name="TypeName"></param>
/// <returns></returns>
public object CreateInstance(string AssemblyFileName, string TypeName)
{
return this.CreateInstance_Internal(AssemblyFileName, TypeName, null);
}
/// <summary>
/// Creates a new instance from a file file based assembly refence. Requires full
/// filename including extension and path.
/// </summary>
/// <param name="AssemblyFileName"></param>
/// <param name="TypeName"></param>
/// <returns></returns>
public object CreateInstance_OneParm(string AssemblyFileName, string TypeName, object Parm1)
{
return this.CreateInstance_Internal(AssemblyFileName, TypeName, Parm1);
}
/// <summary>
/// Creates a new instance from a file file based assembly refence. Requires full
/// filename including extension and path.
/// </summary>
/// <param name="AssemblyFileName"></param>
/// <param name="TypeName"></param>
/// <returns></returns>
public object CreateInstance_TwoParms(string AssemblyFileName, string TypeName, object Parm1, object Parm2)
{
return this.CreateInstance_Internal(AssemblyFileName, TypeName, Parm1, Parm2);
}
/// <summary>
/// Routine that loads a class from an assembly file name specified.
/// </summary>
/// <param name="AssemblyFileName"></param>
/// <param name="TypeName"></param>
/// <returns></returns>
protected object CreateInstanceFromFile_Internal(string AssemblyFileName, string TypeName, params object[] args)
{
this.ErrorMessage = string.Empty;
object server = null;
try
{
if (args == null)
server = AppDomain.CurrentDomain.CreateInstanceFromAndUnwrap(AssemblyFileName, TypeName);
else
server = AppDomain.CurrentDomain.CreateInstanceFromAndUnwrap(AssemblyFileName, TypeName, false, BindingFlags.Default, null, args, null, null, null);
}
catch (Exception ex)
{
this.ErrorMessage = ex.Message;
return null;
}
return server;
}
/// <summary>
/// Routine that loads an assembly by its 'application assembly name' - unsigned
/// assemblies must be visible via the .NET path (current path or BIN dir) and
/// GAC assemblies must be referenced by their full assembly name.
/// </summary>
/// <param name="AssemblyName"></param>
/// <param name="TypeName"></param>
/// <returns></returns>
protected object CreateInstance_Internal(string AssemblyName, string TypeName, params object[] args)
{
this.ErrorMessage = string.Empty;
object server = null;
try
{
if (args == null)
server = AppDomain.CurrentDomain.CreateInstanceAndUnwrap(AssemblyName, TypeName);
else
server = AppDomain.CurrentDomain.CreateInstanceAndUnwrap(AssemblyName, TypeName,false,BindingFlags.Default, null,args,null,null,null);
}
catch(Exception ex)
{
this.ErrorMessage = ex.Message;
return null;
}
return server;
}
}
}
In this version there are multiple versions for each of the CreateInstance and CreateInstanceFrom methods that can take multiple parameters. Although .NET supports method overloading, COM doesn't do a good job of exposing it (it uses a dynamic numbering scheme but it's not obvious which signature gets mapped to which extension number), so rather than using method overloading this code uses explicit method names for each of the parameter signatures. I created three versions for each for none through two parameters in the constructor. This should be sufficient for a large percentage of types.
This is now a custom class and it doesn't need to be registered with COM even though I have the ComVisible and ClassInterface attributes defined. Although the object is not registered through COM the ClassInterface still is useful as it provides Visual FoxPro with Intellisense of types even when 'dynamically' turning objects into IDispatch pointers rather than explicit COM instantiation.
With this code you can now do things like this:
*** Create Bridge object first - hang on for later reuse!
lcError = ""
lnDispHandle = ClrCreateInstanceFrom(FULLPATH("wwDotNetBridge.dll"),"Westwind.WebConnection.wwDotNetBridge",@lcError,@lnSize)
loObject = SYS(3096, lnDispHandle)
? loObject.ToString()
loAct = loObject.CreateInstance_OneParm("System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",;
"System.Text.RegularExpressions.Regex","a.c")
if ISNULL(loAct)
? loObject.ErrorMessage
RETURN
ENDIF
loMatches = loAct.Match("sd-accs.abc.")
? loMatches.Count
The code doesn't look much different than before - the only difference is that we load up an instance of the bridge object first and once that object is loaded it, rather than the C++ DLL code, will be used to instantiate any .NET objects. The result is the same, but you do get more flexibility and better error reporting.
It's nice to see that this works so well with very little effort. I have a number of uses of this right away. I've been very light of my mixing of .NET code and Fox code, but with the ability to directly access .NET DLLs without having to go through COM registration of components makes the prospect of calling into .NET code a whole more useful and efficient. This is especially true for Web applications which due to security environments often cannot access COM objects or more commonly can not easily get COM objects registered on a server.
I've provided the code for both the C++ DLL and the .NET wrapper objects and a rough sample. I haven't extensively tested this stuff out yet, so I'm sure there will be improvements in the future <s>. Also please realise that many .NET objects in the BCL especially do not work well in combination with Visual FoxPro. This especially true of structs, raw array types and nested collections/lists of any type. For this reason you might find that many standard .NET objects don't work with this mechanism directly and you may still need to create wrapper objects. However, given that you can now more easily create these wrappers and call them without registration you're still way ahead <g>...
This is all kind of preliminary, the result of a few hours of experimentation. There are a few other things that would be useful and given then .NET wrapper probably not that difficult to add. For example, it'd be nice to be able to access static functions on things like the Environment class, or utility functions in common libraries. I'll leave that excercise for another day <s>...
You can download the code from:
http://www.west-wind.com/files/tools/misc/clrhost.zip
Other Posts you might also like