The key to running .NET code on the CE web server is loading the server dll into the .NET process. I demonstrated the concept a few years ago to demonstrate this.
Design may look a bit confusing at first glance, but it has several advantages:
- .NET code and unmanaged ISAPI extensions can work side by side on a web server.
- Web server features like encryption and authentication still work
- Resources are still managed by the web server, including the thread pool
- Efficiency likely outperforms any single process and IPC solution
Firstly, we do not need to automatically start the Windows CE web server automatically. Add this to the registry:
[HKEY_LOCAL_MACHINE\Services\HTTPD] "Flags"=dword:4 ; DEVFLAGS_NOLOAD
While we are doing this, add another key to display '/ dotnet' to our custom ISAPI handler:
[HKEY_LOCAL_MACHINE\Comm\HTTPD\VROOTS\/dotnet] @="\\Windows\\HttpdHostUnmanaged.dll"
Now create a .NET exe called HttpdHostProc.exe from the following source code:
using System; using System.Runtime.InteropServices; using System.Text; class HttpdHostProc { static void Main(string[] args) { GetExtensionVersionDelegate pInit = new GetExtensionVersionDelegate(GetExtensionVersion); TerminateExtensionDelegate pDeinit = new TerminateExtensionDelegate(TerminateExtension); HttpExtensionProcDelegate pProc = new HttpExtensionProcDelegate(HttpExtensionProc); Init(pInit, pDeinit, pProc); int state = SERVICE_INIT_STOPPED | SERVICE_NET_ADDR_CHANGE_THREAD; int context = HTP_Init(state); HTP_IOControl(context, IOCTL_SERVICE_REGISTER_SOCKADDR, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero); HTP_IOControl(context, IOCTL_SERVICE_STARTED, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero); RunHttpd(context, 80); } static int GetExtensionVersion(IntPtr pVer) { OutputDebugString("GetExtensionVersion from .NET\r\n"); return 1; } static int TerminateExtension(int dwFlags) { OutputDebugString("TerminateExtension from .NET\r\n"); return 1; } static int HttpExtensionProc(IntPtr pECB) { OutputDebugString("HttpExtensionProc from .NET\r\n"); var response = "<html><head></head><body><p>Hello .NET!</p></body></html>"; var bytes = Encoding.UTF8.GetBytes(response); var length = bytes.Length; var unmanagedbuffer = Marshal.AllocHGlobal(length); Marshal.Copy(bytes, 0, unmanagedbuffer, length); var retval = WriteClient(pECB, unmanagedbuffer, ref length); Marshal.FreeHGlobal(unmanagedbuffer); return retval; } delegate int GetExtensionVersionDelegate(IntPtr pVer); delegate int TerminateExtensionDelegate(int dwFlags); delegate int HttpExtensionProcDelegate(IntPtr pECB); [DllImport("HttpdHostUnmanaged.dll", SetLastError = true)] extern static void Init( GetExtensionVersionDelegate pInit, TerminateExtensionDelegate pDeinit, HttpExtensionProcDelegate pProc ); [DllImport("HttpdHostUnmanaged.dll", SetLastError = true)] extern static int RunHttpd(int context, int port); [DllImport("HttpdHostUnmanaged.dll", SetLastError = true)] extern static int WriteClient(IntPtr pECB, IntPtr Buffer, ref int lpdwBytes); [DllImport("coredll.dll")] extern static void OutputDebugString(string msg); [DllImport("httpd.dll", SetLastError = true)] extern static int HTP_Init(int dwData); [DllImport("httpd.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] extern static bool HTP_IOControl(int dwData, int dwCode, IntPtr pBufIn, int dwLenIn, IntPtr pBufOut, int dwLenOut, IntPtr pdwActualOut); const int IOCTL_SERVICE_STARTED = 0x01040038; const int IOCTL_SERVICE_REGISTER_SOCKADDR = 0x0104002c; const int SERVICE_INIT_STOPPED = 0x00000001; const int SERVICE_NET_ADDR_CHANGE_THREAD = 0x00000008; }
A few comments:
- The main function loads and initializes our unmanaged dll, which will act as a step between the managed and unmanaged code.
- Then it initializes and starts the web server, calling its function HTP_Init, and then a couple of ioctls
- Finally, it calls RunHttpd in our unmanaged dll, which will accept incoming requests and redirect them to the web server.
The three functions below - GetExtensionVersion, TerminateExtension, HttpExtensionProc - should look familiar if you've ever done any ISAPI programs. If not, all you really need to know is that incoming requests are handled by HttpExtensionProc.
Going to an unmanaged dll, HttpdHostUnmanaged.dll:
#include <winsock2.h> #include <httpext.h> typedef BOOL (* pfHTP_IOControl)(DWORD dwData, DWORD dwCode, PBYTE pBufIn, DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut, PDWORD pdwActualOut); typedef BOOL (* PFN_WRITE_CLIENT)(HCONN ConnID, LPVOID Buffer, LPDWORD lpdwBytes, DWORD dwReserved); static PFN_GETEXTENSIONVERSION g_pInit; static PFN_TERMINATEEXTENSION g_pDeinit; static PFN_HTTPEXTENSIONPROC g_pProc; BOOL APIENTRY DllMain(HANDLE, DWORD, LPVOID) { return TRUE; } extern "C" void Init( PFN_GETEXTENSIONVERSION pInit, PFN_TERMINATEEXTENSION pDeinit, PFN_HTTPEXTENSIONPROC pProc) { // Store pointers to .NET delegates g_pInit = pInit; g_pDeinit = pDeinit; g_pProc = pProc; } extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) { pVer->dwExtensionVersion = HSE_VERSION; strncpy(pVer->lpszExtensionDesc, "HttpdHostUnmanaged", HSE_MAX_EXT_DLL_NAME_LEN); // Call .NET GetExtensionVersion return g_pInit(pVer); } extern "C" BOOL WINAPI TerminateExtension(DWORD dwFlags) { // Call .NET TerminateExtension return g_pDeinit(dwFlags); } extern "C" DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) { // Call .NET HttpExtensionProc return g_pProc(pECB); } extern "C" DWORD WINAPI WriteClient(EXTENSION_CONTROL_BLOCK *pECB, LPVOID Buffer, LPDWORD lpdwBytes) { return pECB->WriteClient(pECB->ConnID, Buffer, lpdwBytes, 0); } extern "C" int WINAPI RunHttpd(DWORD context, int port) { // Load web server and start accepting connections. // When a connection comes in, // pass it to httpd using IOCTL_SERVICE_CONNECTION. HMODULE hDll = LoadLibrary(L"httpd.dll"); if(!hDll) { return -1; } pfHTP_IOControl Ioctl = (pfHTP_IOControl)GetProcAddress(hDll, L"HTP_IOControl"); if(!Ioctl) { FreeLibrary(hDll); return -2; } WSADATA Data; int status = WSAStartup(MAKEWORD(1, 1), &Data); if(status != 0) { FreeLibrary(hDll); return status; } SOCKET s = socket(PF_INET, SOCK_STREAM, 0); if(s == INVALID_SOCKET) { status = WSAGetLastError(); goto exit; } SOCKADDR_IN sAddr; memset(&sAddr, 0, sizeof(sAddr)); sAddr.sin_port = htons(port); sAddr.sin_family = AF_INET; sAddr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(s, (LPSOCKADDR)&sAddr, sizeof(sAddr)) == SOCKET_ERROR) { status = WSAGetLastError(); goto exit; } if(listen(s, SOMAXCONN) == SOCKET_ERROR) { status = WSAGetLastError(); goto exit; } for(;;) { SOCKADDR_IN addr; int cbAddr = sizeof(addr); SOCKET conn = accept(s, (LPSOCKADDR)&addr, &cbAddr); if(conn == INVALID_SOCKET) { status = WSAGetLastError(); goto exit; } DWORD IOCTL_SERVICE_CONNECTION = 0x01040034; Ioctl(context, IOCTL_SERVICE_CONNECTION, (PBYTE)&conn, sizeof(conn), NULL, 0, NULL); } exit: FreeLibrary(hDll); if(s != INVALID_SOCKET) { closesocket(s); } WSACleanup(); return status; }
There are a number of not-so-interesting features that redirect calls to and from .NET.
As mentioned above, the RunHttpd function simply accepts incoming connections and passes them to the web server for processing using another ioctl.
To verify all this, run HttpdHostProc.exe on the device, then open http://<ipaddr>/dotnet in a browser. The CE device should respond with a bit of HTML containing the message "Hello.NET!"
This code runs on Windows Embedded Compact 7.0 with the .NET Compact Framework 3.5, but is likely to work with other versions.
I built an unmanaged dll against Pocket PC 2003 SDK, as this is what I accidentally installed. Probably any Windows CE SDK will do, but you may have to configure the compiler options, for example, I had to build with / GS - (buffer security checks are disabled) for PPC2003.
It is tempting to implement the RunHttpd function in .NET, but be warned that there are several possible problems with this:
- In my testing, the Handle property on the .NET CF socket returned a pseudo descriptor that did not work with socket APIs
- The socket lifetime will be controlled by the .NET runtime, which makes it difficult to transfer ownership of the socket to the web server.
If you don't mind compiling with / unsafe, performance can probably be improved a bit by passing fixed buffers to WriteClient rather than copying all the response data to an unmanaged buffer in HttpExtensionProc.
The EXTENSION_CONTROL_BLOCK structure contains a number of useful fields and functions that, obviously, should be included in the full implementation.
EDIT
Just to clarify how requests are handled:
- Incoming requests are received at RunHttpd, which redirects them to the web server using ioctl
- According to the introduction of vroot in the registry that we installed earlier, the web server calls HttpdHostUnmanaged.dll to process requests for / dotnet
- If this is the first request for / dotnet, the web server starts by invoking an unmanaged version of GetExtensionVersion in HttpdHostUnmanaged.dll. The unmanaged GetExtensionVersion returns back to the .NET version of GetExtensionVersion. GetExtensionVersion is a convenient place to initialize any necessary resources, since it is called only once (the corresponding cleaning function is TerminateExtension, which is called when / if the web server decides to unload HttpdHostUnmanaged.dll)
- The web server then calls the unmanaged HttpExtensionProc
- Unmanaged HttpExtensionProc accesses the .NET version of HttpExtensionProc
- Managed HttpExtensionProc generates a response and calls an unmanaged WriteClient to deliver to its client