Mongoose for Unity

Mongoose is a C library for embedding a simple web server in another application. We used Mongoose for all of our web-enabled Torque games and I wanted to be able to continue using it for new games in Unity.

There is an existing extension called UniWeb which provides a web server in unity, but their code doesn’t support web sockets. I’ve just built Mongoose in windows, but it is Linux and Mac compatible as well. However, it wont support any of the mobile platforms.

This post will describe the steps for building Mongoose 5.6 as a windows native DLL, then wrapping it for use in C#, and finally including it in a Unity project.

To create a DLL from the mongoose source code available on github, I created a DLL project in visual studio, set it to 64 bit, and added __declspec(dllexport) declarations for all the functions I want available from C#.

The C# wrapper requires defining all the data types that are passed to the exported functions. There are two data structures declared in the C code which look like this:


struct mg_connection {
const char *request_method; // "GET", "POST", etc
const char *uri; // URL-decoded URI
const char *http_version; // E.g. "1.0", "1.1"
const char *query_string; // URL part after '?', not including '?', or NULL

char remote_ip[48]; // Max IPv6 string length is 45 characters
char local_ip[48]; // Local IP address
unsigned short remote_port; // Client's port
unsigned short local_port; // Local port number

int num_headers; // Number of HTTP headers
struct mg_header {
  const char *name; // HTTP header name
  const char *value; // HTTP header value
} http_headers[30];

char *content; // POST (or websocket message) data, or NULL
size_t content_len; // Data length

int is_websocket; // Connection is a websocket connection
int status_code; // HTTP status code for HTTP error handler
int wsbits; // First byte of the websocket frame
void *server_param; // Parameter passed to mg_create_server()
void *connection_param; // Placeholder for connection-specific data
void *callback_param;
};

I am building 64 bit applications, so the C# wrapper for this structure looks like this:


// mongoose.h :: struct mg_connection { struct mg_header }
[StructLayout(LayoutKind.Sequential)]
public struct MongooseHeader
{
  [MarshalAs(UnmanagedType.LPStr)]
  public string name;
  [MarshalAs(UnmanagedType.LPStr)]
  public string value;
};

// mongoose.h :: struct mg_connection
[StructLayout(LayoutKind.Sequential)]
public struct MongooseConnection
{
[MarshalAs(UnmanagedType.LPStr)]
public string request_method;
[MarshalAs(UnmanagedType.LPStr)]
public string uri;
[MarshalAs(UnmanagedType.LPStr)]
public string http_version;
[MarshalAs(UnmanagedType.LPStr)]
public string query_string;

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)]
public char[] remote_ip;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)]
public char[] local_ip;
[MarshalAs(UnmanagedType.U2)]
public UInt16 remote_port;
[MarshalAs(UnmanagedType.U2)]
public UInt16 local_port;

[MarshalAs(UnmanagedType.I4)]
public int num_headers;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)]
public MongooseHeader[] http_headers;

public IntPtr content;
public System.UInt64 content_len;

[MarshalAs(UnmanagedType.I4)]
public int is_websocket;
[MarshalAs(UnmanagedType.I4)]
public int status_code;
[MarshalAs(UnmanagedType.I4)]
public int wsbits;

public IntPtr server_param;
public IntPtr connection_param;
public IntPtr callback_param;
};

Anything that is a char* gets converted to a string, unsigned short becomes UInt16, int is the same, but needs to be marked as I4 to have the right size. The void* pointers are converted to IntPtr which is the C# unmanaged pointer type.

I replicated the enums that I’m going to use in C#:


    public enum MongooseResult { MG_FALSE, MG_TRUE, MG_MORE };
    private enum MongooseEvent
    {
      MG_POLL = 100,  // If callback returns MG_TRUE connection closes
                      // after all of data is sent
      MG_CONNECT,     // If callback returns MG_FALSE, connect fails
      MG_AUTH,        // If callback returns MG_FALSE, authentication fails
      MG_REQUEST,     // If callback returns MG_FALSE, Mongoose continues with req
      MG_REPLY,       // If callback returns MG_FALSE, Mongoose closes connection
      MG_RECV,        // Mongoose has received POST data chunk.
                      // Callback should return a number of bytes to discard from
                      // the receive buffer, or -1 to close the connection.
      MG_CLOSE,       // Connection is closed, callback return value is ignored
      MG_WS_HANDSHAKE,  // New websocket connection, handshake request
      MG_WS_CONNECT,  // New websocket connection established
      MG_HTTP_ERROR   // If callback returns MG_FALSE, Mongoose continues with err
    };

    // Possible websocket opcodes. These are available as the right four bits of
    // the websocket status in the connection after a MG_REQUEST coming from a
    // websocket. The first four bits are:
    // bit 0. 1 if this is the last frame in the message
    // bits 1-3. reserved
    private enum MongooseWebsockOpcode
    {
      WEBSOCKET_OPCODE_CONTINUATION = 0x0,
      WEBSOCKET_OPCODE_TEXT = 0x1,
      WEBSOCKET_OPCODE_BINARY = 0x2,
      WEBSOCKET_OPCODE_CONNECTION_CLOSE = 0x8,
      WEBSOCKET_OPCODE_PING = 0x9,
      WEBSOCKET_OPCODE_PONG = 0xa
    };

The functions are treated much like the data structures. The original function signatures from the C code:

typedef int (* mg_handler_t)(struct mg_connection *, enum mg_event);
// Server management functions
DLL_API struct mg_server * mg_create_server(void *server_param, mg_handler_t handler);
DLL_API time_t mg_poll_server(struct mg_server *, int milliseconds);
DLL_API const char * mg_set_option(struct mg_server *, const char *opt, const char *val);
DLL_API const char * mg_get_option(const struct mg_server *server, const char *name);
DLL_API void mg_send_status(struct mg_connection *, int status_code);
DLL_API void mg_send_header(struct mg_connection *, const char *name, const char *val);
DLL_API size_t mg_send_data(struct mg_connection *, const void *data, int data_len);
DLL_API size_t mg_websocket_write(struct mg_connection *, int opcode,
                                  const char *data, size_t data_len);
DLL_API void mg_destroy_server(struct mg_server *);
DLL_API struct mg_connection * mg_next(struct mg_server *, struct mg_connection *);

Are wrapped as static extern functions in C#:

[DllImport("Mongoose", CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr mg_create_server(IntPtr user_data, MongooseEventHandler eh);
[DllImport("Mongoose", CallingConvention = CallingConvention.StdCall)]
private static extern int mg_poll_server(IntPtr server, int milli);
[DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
private static extern IntPtr mg_set_option(IntPtr server, string name, string value);
[DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
private static extern IntPtr mg_get_option(IntPtr server, string name);

[DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
private static extern ulong mg_send_header(IntPtr conn, string name, string value);
// Note: This could be binary data in the C++ code.
[DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
private static extern ulong mg_send_data(IntPtr conn, string data, int length);
[DllImport("Mongoose", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public static extern ulong mg_websocket_write(IntPtr conn, int opCode, string data, ulong dataLength);
[DllImport("Mongoose", CallingConvention = CallingConvention.StdCall)]
private static extern void mg_destroy_server(IntPtr server);

[DllImport("Mongoose", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr mg_next(IntPtr server, IntPtr connection);

Most of this is standard. I am using the StdCall calling convention and the Ansi character set since this is calling C code instead of C++. The mg_create_server function takes an event handler callback function and returns a pointer to the server. The C library manages the memory for the server pointer, but the event handler that the C# code creates to pass to this function needs to be protected from garbage collection.

The code to do this looks like:

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate int MongooseEventHandler(IntPtr c, int ev);

private IntPtr _server;
private MongooseEventHandler _delegateInstance;

public bool startWebServer(string documentRoot, int port)
{
  _delegateInstance = new MongooseEventHandler(EventHandler);
  _server = mg_create_server(IntPtr.Zero, _delegateInstance);
}

Another bit of conversion code is required for any data that is passed to the C# code by pointer from the C code. Examples are the mg_get_option function that returns a const char* and the event handler that takes a takes an mg_connection*. This memory can’t be accessed directly by the managed C# code, so it need to be Marshaled (copied) into managed memory like this:

public string getOption(string option)
{
  return Marshal.PtrToStringAnsi(mg_get_option(_server, option));
}
private int EventHandler(IntPtr conn_ptr, int ev)
{
  MongooseConnection conn = (MongooseConnection)
    System.Runtime.InteropServices.Marshal.PtrToStructure(
      conn_ptr, typeof(MongooseConnection));
...
}

I wrote a C# class to wrap these functions and to do some basic connection management and provide default behavior. The wrapper class will keep a list of connections that are active so that it can detect dropped connections during the poll. I also abstracted away the mg_connection structure for most functions and just pass the client’s port number as the key to reference a connection. I also setup some defaults (no authentication, default error handling and serving up files from the local filesystem under a given web root). Here is the whole class is you are interested: Mongoose.cs (there is a tiny bit of Unity logging code, if you aren’t using Unity, just remove those lines)

Finally, to use this web server in Unity, I just had to create a behavior that starts, stops and polls the web server using the wrapper class. For default behavior, this code looks like:

public class TestWebserver : MonoBehaviour {

  class WebSocketServer : Mongoose.WebServer
  {
     // To create custom behavior, override the default handlers here
  }
  private WebSocketServer _webServer;

  void Start () {
    Debug.Log("Starting webserver");
    _webServer = new WebSocketServer(this);
    _webServer.startWebServer("web", 8080);
  }
	
  // Update is called once per frame. Poll the webserver, don't wait for connections, just proccess what is already waiting
  void Update () {
    _webServer.poll(0);
  }

  void OnDestroy()
  {
    Debug.Log("Stoping webserver");
    _webServer.close();
  }
}

Add this behavior to any game object and you have a webserver serving files from <Unity Application Dir>/web on port 8080. A little bit more code allows for websocket connection handling and communication.

One thought on “Mongoose for Unity”

Leave a Reply