NET:Inner Space Bootstrap

From Lavish Software Wiki
Jump to navigation Jump to search

Overview

This article explains one process of exposing unmanaged API to .NET programs, the Inner Space bootstrap. This method uses a GetProcAddress-like mechanism that allows function versioning, and does not require DLL exports. Instead, a library is registered with Inner Space, which can be queried via LavishVM.GetAPI.

The Process

Create a .NET class library

First, select a .NET language to create the .NET portion of the API. Any language will do, as .NET libraries are designed to work across all .NET languages. I recommend C# and will use C# for examples here (though equivalents in other languages will surely be added), but again, any language will work. After selecting a language, create a class library (note: for some languages, such as C++, when creating the project in Visual Studio, you need to select the project type under CLR). This library is to be linked against by developers wishing to use your API, although they could also create their own library with essentially the same code in it for the same purpose.

Your class library should first of all provide a set of delegates that will correspond directly to equivalent API in your C++ Inner Space extension. Therefore, the prototype should exactly match the internal function.

For example
public delegate void Echo(string Output); // C# delegate
static void __stdcall Echo(const char *Output); // unmanaged C++ equivalent

Delegates default to the stdcall calling convention, but can be modified with attributes to use other calling conventions, such as cdecl. It is recommended that the C++ API function use stdcall.

After creating the delegates, the .NET API can be implemented by instantiating the delegate, with a delegate that attaches to your function by pointer. The function pointer is retrieved at runtime by calling LavishVM.GetAPI. The recommended implementation for this is to create a stub function, which will attempt to attach the API and call it, or generally return a failure result if the API could not be attached.

For Example
        static public InnerSpaceAPI.Delegates.Echo Echo = Stubs.Echo;
            static public void Echo(string Output)
            {
                IntPtr Address = LavishVM.GetAPI("Inner Space", "Echo", 1);
                if (Address.ToInt32() == 0)
                {
                    Console.WriteLine(Output);
                    return;
                }
                InnerSpace.Echo = (InnerSpaceAPI.Delegates.Echo)Marshal.GetDelegateForFunctionPointer(Address, typeof(InnerSpaceAPI.Delegates.Echo));
                InnerSpace.Echo(Output);
            }

This particular case is a special case, in which the functionality of Echo uses a surrogate: Console.WriteLine. If the API function has a return value, return the value from the stub, like this:

return InnerSpace.Echo(Output); // Note: InnerSpace.Echo has a void return value, therefore this will fail to compile.

With this implementation, the stub automatically instantiates the delegate in the Echo field with the function pointer returned by LavishVM.GetAPI, although Echo originally pointed to the stub.

A problem would occur here if the C++ library is unloaded after API is attached -- attempting to continue to use the API would cause a crash. Therefore a final portion of the .NET API, if the C++ API can be unloaded, should detach all of the API, and replace the value with the original stub. This can be implemented via Events, or otherwise calling a function in the .NET API from the C++ API at the time of unloading. Care should be taken to ensure that the C++ API is not currently in context when the DLL is unloaded.

Recommended API layout

This system is designed to work very similar to using the System libraries, in that everything can be transparent to the target developer. However, this implementation imposes a few restrictions due to the way that namespaces work. Namespaces are not allowed to contain fields, so each function implemented with our method must be a static public member of a class. The use of nested classes should be limited, as class usage cannot be implicit through the usage keyword.

using System.Runtime.InteropServices;
using LavishVMAPI; // allows using LavishVMAPI.LavishVM.GetAPI without explicitly specifying LavihsVMAPI
namespace FooAPI
{
    namespace Delegates
    {
        public delegate void Bar();
    }

    public class Foo
    {
        public static FooAPI.Delegates.Bar Bar = Stubs.Bar;

        internal class Stubs
        {
            static public void Bar()
            {
                IntPtr Address = LavishVM.GetAPI("Foo", "Bar", 1);
                if (Address.ToInt32() == 0)
                    return;
                Foo.Bar = (FooAPI.Delegates.Bar)Marshal.GetDelegateForFunctionPointer(Address, typeof(FooAPI.Delegates.Bar));
                Foo.Bar();
            }
        }

        public class FooChildClass
        {
            /* same fashion as Foo */
        }
    }

    public class AdditionalFooClass
    {
        /* same fashion as Foo */
    }
}

Create equivalent API in Inner Space extension

Implementing the extension API requires ISXDK v29d or later

Implementing the unmanaged portion of the API is only a matter of producing a function that matches the delegate used in managed code, and providing the function for Inner Space to use for LavishVM.GetAPI calls specifying your library. When registering your library, you must provide a function that follows this format:

void * __stdcall GetAPI(const char *Name, unsigned int Version);

The implemented GetAPI function should retrieve a pointer to an API function, given the Name of the API. The Version parameter can be used to provide per-function versioning, in other words, can deny a given version or return a version-specific API function.

Example

The example below does not implement versioning, and uses a std::map to store the list of function names and map them to a function. The example uses a class with static member functions, but this can also be implemented with a namespace instead, or without either since no class is required and all that is necessary is a few stdcall functions.

#include <map>
using namespace std;
class InnerSpaceAPI
{
public:
	/* API Mapper */
	static void * __stdcall GetAPI(const char *Name, unsigned int Version)
	{
		static map<string,void*> APIMap;
		static bool bInitialized=false;
		/* initialize map */
		if (!bInitialized)
		{
			APIMap["Echo"]=Echo;

			bInitialized=true;
		}

 		/* map has been initialized, retrieve function */
		map<string,void*>::iterator i=APIMap.find(Name);
		if (i==APIMap.end())
			return 0;
		return i->second;
	}

	/* API */
	static void __stdcall Echo(const char *Output)
	{
		pISInterface->FrameLock();
		printf("%s",Output);
		pISInterface->FrameUnlock();
	}
};

ISInterface::FrameLock and FrameUnlock can be used, as shown in the Echo function, to acquire frame lock. Strategic use of frame locking creates implicit frame safety for .NET developers, so that they do not need to use a frame lock for a single API call that requires it.

Register the library

ISInterface::RegisterLibrary is used to attach a GetAPI function to the name of a library.

Example

This example goes with the previous example, which implemented a class with static member functions.

pISInterface->RegisterLibrary("Inner Space",InnerSpaceAPI::GetAPI);

Library naming schemes

There is no "right" or "wrong" way to name your unmanaged library and individual unmanaged API functions for mapping. It is recommended that you use a library name matching your outermost namespace in the .NET API, and function name strings should generally match the name of your function. Keep in mind that the names used for mapping are internal, and only need to match between your .NET API stubs (the LavishVM.GetAPI call), the RegisterLibrary call, and GetAPI implementations in the unmanaged code, so they will logically be hidden from other developers -- in other words, the names you use internally are in fact arbitrary, as long as they match.

Child classes and namespaces can be mapped at your discretion. You may choose to register each class as a library, each with its own GetAPI function. You may choose to use the same library, but different API names. There is no right or wrong way. The speed difference between the two is negligible, as the mapping will only take place once per function, per instance of your .NET library, so it really depends on your preference as far as implementing multiple GetAPI functions, or just one.

Examples

  • Library name "FooAPI", function name "Foo.Bar"
  • Library name "FooAPI.Foo", function name "Bar"
  • Library name "Foo" function name "FooAPI.Foo.Bar"
  • Library name "FooAPI", function name "61738d41daeabe92ab39c612631c270c" -- that's MD5("Foo.Bar FOO SECRET KEY")

Protecting internal function names

If you do not want to expose the name of an internal function for any reason (and thusly a way for any wannabe hacker to easily discern the location of API in your closed source extension), you can use a naming scheme that does not involve the name of the function, such as a hash value. For example, the MD5 hash of (name of function + secret key) would be impossible for anyone to crack to get the name of the internal function. Granted, the GetAPI call can still be traced from your stubs to determine which stub is what function, but this is at least some degree of protection. There are more advanced ways to implement this sort of protection if you so desire.

See Also