瀏覽代碼

[wasm][bindings] Add support for strongly typed marshalling of C# enums. (#10958)

* Add support for strongly typed marshalling of C# enums.

JavaScript does not have the concept of Enums like TypeScript.  Strings are used to represent enums.  This PR allows a strongly type contract between C# values and the string representations used by a lot of the JavaScript API's.

Example:

DOM InputElement type ->  https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-type#Form_<input>_types

    The InputElement types are represented by strings.  button, checkbox, color, date, datetime-local etc....

     These string can be represented and marshalled back and forth by defining an enum as shown below with an `Export` data contract attribute.

```
    public enum InputElementType
    {
        [Export(EnumValue = ConvertEnum.ToLower)]
        Button,
        [Export(EnumValue = ConvertEnum.ToLower)]
        CheckBox,
        [Export(EnumValue = ConvertEnum.ToLower)]
        Color,
        [Export(EnumValue = ConvertEnum.ToLower)]
        Date,
        [Export("datetime-local")]
        DateTimeLocal,
   }

```

The enum conversion allows controlling the marshalling:

- Default - Marshals the enum name as is
- ToLower - Marshals the enum name to lower case string
- ToUpper - Marshals the enum name to upper case string
- Numeric - Marshals the enum numeric values.

Marshalling from C# to JavaScript using the SDK Bindings should be made as easy and pain free as possible.

Marshalling the string values from JavaScript to C# will need a little help.  The SDK Bindings Runtime provides a method to transport the string value to a strongly typed enum.

```
   WebAssembly.Runtime.EnumFromExportContract(typeof(InputElementType), inputElementType);
```

* Add marshalling of C# Enums to method calls.

* Add tests to bindings-test harness for testing marshalling of C# enum values.

* Modify test for marshalling enum value without Export contract.
Kenneth Pouncey 7 年之前
父節點
當前提交
a7f5952c69
共有 5 個文件被更改,包括 369 次插入71 次删除
  1. 27 64
      sdks/wasm/WasmHttpMessageHandler.cs
  2. 50 5
      sdks/wasm/binding_support.js
  3. 73 0
      sdks/wasm/bindings-test.cs
  4. 183 2
      sdks/wasm/bindings.cs
  5. 36 0
      sdks/wasm/driver.c

+ 27 - 64
sdks/wasm/WasmHttpMessageHandler.cs

@@ -68,9 +68,18 @@ namespace WebAssembly.Net.Http.HttpClient
             {
                 var requestObject = (JSObject)json.Invoke("parse", "{}");
                 requestObject.SetObjectProperty("method", request.Method.Method);
-                requestObject.SetObjectProperty("credentials", GetDefaultCredentialsString());
-                requestObject.SetObjectProperty("cache", GetCacheModeString());
-                requestObject.SetObjectProperty("mode", GetRequestModeString());
+
+                // See https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials for
+                // standard values and meanings
+                requestObject.SetObjectProperty("credentials", DefaultCredentials);
+
+                // See https://developer.mozilla.org/en-US/docs/Web/API/Request/cache for
+                // standard values and meanings
+                requestObject.SetObjectProperty("cache", Cache);
+
+                // See https://developer.mozilla.org/en-US/docs/Web/API/Request/mode for
+                // standard values and meanings
+                requestObject.SetObjectProperty("mode", Mode);
 
                 // We need to check for body content
                 if (request.Content != null)
@@ -171,67 +180,7 @@ namespace WebAssembly.Net.Http.HttpClient
 
 
         }
-
-        private static string GetDefaultCredentialsString()
-        {
-            // See https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials for
-            // standard values and meanings
-            switch (DefaultCredentials)
-            {
-                case FetchCredentialsOption.Omit:
-                    return "omit";
-                case FetchCredentialsOption.SameOrigin:
-                    return "same-origin";
-                case FetchCredentialsOption.Include:
-                    return "include";
-                default:
-                    throw new ArgumentException($"Unknown credentials option '{DefaultCredentials}'.");
-            }
-        }
-
-
-        private static string GetCacheModeString()
-        {
-            // See https://developer.mozilla.org/en-US/docs/Web/API/Request/cache for
-            // standard values and meanings
-            switch (Cache)
-            {
-                case RequestCache.Default:
-                    return "default";
-                case RequestCache.NoStore:
-                    return "no-store";
-                case RequestCache.Reload:
-                    return "reload";
-                case RequestCache.NoCache:
-                    return "no-cache";
-                case RequestCache.ForceCache:
-                    return "force-cache";
-                case RequestCache.OnlyIfCached:
-                    return "only-if-cached";
-                default:
-                    throw new ArgumentException($"Unknown cache option '{Mode}'.");
-            }
-        }
-
-        private static string GetRequestModeString()
-        {
-            // See https://developer.mozilla.org/en-US/docs/Web/API/Request/mode for
-            // standard values and meanings
-            switch (Mode)
-            {
-                case RequestMode.Cors:
-                    return "cors";
-                case RequestMode.Navigate:
-                    return "navigate";
-                case RequestMode.NoCors:
-                    return "no-cors";
-                case RequestMode.SameOrigin:
-                    return "same-origin";
-                default:
-                    throw new ArgumentException($"Unknown request mode '{Mode}'.");
-            }
-        }
-
+        
         private string[][] GetHeadersAsStringArray(HttpRequestMessage request)
             => (from header in request.Headers.Concat(request.Content?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
                     from headerValue in header.Value // There can be more than one value for each name
@@ -299,21 +248,25 @@ namespace WebAssembly.Net.Http.HttpClient
         /// <summary>
         /// Advises the browser never to send credentials (such as cookies or HTTP auth headers).
         /// </summary>
+        [Export(EnumValue = ConvertEnum.ToLower)]
         Omit,
 
         /// <summary>
         /// Advises the browser to send credentials (such as cookies or HTTP auth headers)
         /// only if the target URL is on the same origin as the calling application.
         /// </summary>
+        [Export("same-origin")]
         SameOrigin,
 
         /// <summary>
         /// Advises the browser to send credentials (such as cookies or HTTP auth headers)
         /// even for cross-origin requests.
         /// </summary>
+        [Export(EnumValue = ConvertEnum.ToLower)]
         Include,
     }
 
+
     /// <summary>
     /// The cache mode of the request. It controls how the request will interact with the browser's HTTP cache.
     /// </summary>
@@ -322,34 +275,40 @@ namespace WebAssembly.Net.Http.HttpClient
         /// <summary>
         /// The browser looks for a matching request in its HTTP cache.
         /// </summary>
+        [Export(EnumValue = ConvertEnum.ToLower)]
         Default,
 
         /// <summary>
         /// The browser fetches the resource from the remote server without first looking in the cache, 
         /// and will not update the cache with the downloaded resource.
         /// </summary>
+        [Export("no-store")]
         NoStore,
 
         /// <summary>
         /// The browser fetches the resource from the remote server without first looking in the cache, 
         /// but then will update the cache with the downloaded resource.
         /// </summary>
+        [Export(EnumValue = ConvertEnum.ToLower)]
         Reload,
 
         /// <summary>
         /// The browser looks for a matching request in its HTTP cache.
         /// </summary>
+        [Export("no-cache")]
         NoCache,
 
         /// <summary>
         /// The browser looks for a matching request in its HTTP cache.
         /// </summary>
+        [Export("force-cache")]
         ForceCache,
 
         /// <summary>
         /// The browser looks for a matching request in its HTTP cache.
         /// Mode can only be used if the request's mode is "same-origin"
         /// </summary>
+        [Export("only-if-cached")]
         OnlyIfCached,
     }
 
@@ -361,22 +320,26 @@ namespace WebAssembly.Net.Http.HttpClient
         /// <summary>
         /// If a request is made to another origin with this mode set, the result is simply an error
         /// </summary>
+        [Export("same-origin")]
         SameOrigin,
 
         /// <summary>
         /// Prevents the method from being anything other than HEAD, GET or POST, and the headers from 
         /// being anything other than simple headers.
         /// </summary>
+        [Export("no-cors")]
         NoCors,
 
         /// <summary>
         /// Allows cross-origin requests, for example to access various APIs offered by 3rd party vendors. 
         /// </summary>
+        [Export(EnumValue = ConvertEnum.ToLower)]
         Cors,
 
         /// <summary>
         /// A mode for supporting navigation.
         /// </summary>
+        [Export(EnumValue = ConvertEnum.ToLower)]
         Navigate,
     }
 

+ 50 - 5
sdks/wasm/binding_support.js

@@ -6,6 +6,7 @@ var BindingSupportLib = {
 		mono_wasm_object_registry: [],
 		mono_wasm_ref_counter: 0,
 		mono_wasm_free_list: [],
+		mono_wasm_marshal_enum_as_int: false,	
 		mono_bindings_init: function (binding_asm) {
 			this.BINDING_ASM = binding_asm;
 		},
@@ -36,6 +37,7 @@ var BindingSupportLib = {
 			this.mono_array_get = Module.cwrap ('mono_wasm_array_get', 'number', ['number', 'number']);
 			this.mono_obj_array_new = Module.cwrap ('mono_wasm_obj_array_new', 'number', ['number']);
 			this.mono_obj_array_set = Module.cwrap ('mono_wasm_obj_array_set', 'void', ['number', 'number', 'number']);
+			this.mono_unbox_enum = Module.cwrap ('mono_wasm_unbox_enum', 'number', ['number']);
 
 			// receives a byteoffset into allocated Heap with a size.
 			this.mono_typed_array_new = Module.cwrap ('mono_wasm_typed_array_new', 'number', ['number','number','number','number']);
@@ -88,6 +90,10 @@ var BindingSupportLib = {
 			this.tcs_get_task_and_bind = get_method ("GetTaskAndBind");
 			this.get_call_sig = get_method ("GetCallSignature");
 
+			this.object_to_string = get_method ("ObjectToString");
+
+			this.object_to_enum = get_method ("ObjectToEnum");
+
 			this.init = true;
 		},		
 
@@ -173,6 +179,20 @@ var BindingSupportLib = {
 			case 8: // bool
 				return this.mono_unbox_int (mono_obj) != 0;
 
+			case 9: // enum
+
+				if(this.mono_wasm_marshal_enum_as_int)
+				{
+					return this.mono_unbox_enum (mono_obj);
+				}
+				else
+				{
+					enumValue = this.call_method(this.object_to_string, null, "m", [ mono_obj ]);
+				}
+
+				return enumValue;
+
+
 			case 11: 
 			case 12: 
 			case 13: 
@@ -303,15 +323,15 @@ var BindingSupportLib = {
 
 			if (js_obj == null || js_obj == undefined)
 				return 0;
-			if (typeof js_obj == 'number') {
+			if (typeof js_obj === 'number') {
 				if (parseInt(js_obj) == js_obj)
 					return this.call_method (this.box_js_int, null, "im", [ js_obj ]);
 				return this.call_method (this.box_js_double, null, "dm", [ js_obj ]);
 			}
-			if (typeof js_obj == 'string')
+			if (typeof js_obj === 'string')
 				return this.js_string_to_mono_string (js_obj);
 
-			if (typeof js_obj == 'boolean')
+			if (typeof js_obj === 'boolean')
 				return this.call_method (this.box_js_bool, null, "im", [ js_obj ]);
 
 			if (Promise.resolve(js_obj) === js_obj) {
@@ -382,7 +402,18 @@ var BindingSupportLib = {
 
 			return this.extract_mono_obj (js_obj);
 		},
+		js_to_mono_enum: function (method, parmIdx, js_obj) {
+			this.bindings_lazy_init ();
+    
+			if (js_obj === null || typeof js_obj === "undefined")
+				return 0;
 
+			var monoObj = this.js_to_mono_obj(js_obj);
+			// Check enum contract
+			var monoEnum = this.call_method(this.object_to_enum, null, "iimm", [ method, parmIdx, monoObj ])
+			// return the unboxed enum value.
+			return this.mono_unbox_enum(monoEnum);
+		},
 		wasm_binding_obj_new: function (js_obj_id)
 		{
 			return this.call_method (this.bind_js_obj, null, "i", [js_obj_id]);
@@ -484,7 +515,9 @@ var BindingSupportLib = {
 		args_marshal is a string with one character per parameter that tells how to marshal it, here are the valid values:
 
 		i: int32
-		l: int64
+		j: int32 - Enum with underlying type of int32
+		l: int64 
+		k: int64 - Enum with underlying type of int64
 		f: float
 		d: double
 		s: string
@@ -499,7 +532,7 @@ var BindingSupportLib = {
 			var extra_args_mem = 0;
 			for (var i = 0; i < args.length; ++i) {
 				//long/double memory must be 8 bytes aligned and I'm being lazy here
-				if (args_marshal[i] == 'i' || args_marshal[i] == 'f' || args_marshal[i] == 'l' || args_marshal[i] == 'd')
+				if (args_marshal[i] == 'i' || args_marshal[i] == 'f' || args_marshal[i] == 'l' || args_marshal[i] == 'd' || args_marshal[i] == 'j' || args_marshal[i] == 'k')
 					extra_args_mem += 8;
 			}
 
@@ -514,6 +547,18 @@ var BindingSupportLib = {
 					Module.setValue (args_mem + i * 4, args [i], "i32");
 				} else if (args_marshal[i] == 'o') {
 					Module.setValue (args_mem + i * 4, this.js_to_mono_obj (args [i]), "i32");
+				} else if (args_marshal[i] == 'j'  || args_marshal[i] == 'k') {
+					var enumVal = this.js_to_mono_enum(method, i, args[i]);
+		
+					var extra_cell = extra_args_mem + extra_arg_idx;
+					extra_arg_idx += 8;
+
+					if (args_marshal[i] == 'j')
+						Module.setValue (extra_cell, enumVal, "i32");
+					else if (args_marshal[i] == 'k')
+						Module.setValue (extra_cell, enumVal, "i64");
+
+					Module.setValue (args_mem + i * 4, extra_cell, "i32");
 				} else if (args_marshal[i] == 'i' || args_marshal[i] == 'f' || args_marshal[i] == 'l' || args_marshal[i] == 'd') {
 					var extra_cell = extra_args_mem + extra_arg_idx;
 					extra_arg_idx += 8;

+ 73 - 0
sdks/wasm/bindings-test.cs

@@ -287,6 +287,29 @@ public class TestClass {
 		client = new HttpClient();
 	}	
 
+	public static RequestCache[] requestEnums;
+
+	public static void SetRequestEnums (RequestCache dflt, RequestCache nostore, RequestCache reload, RequestCache nocache, RequestCache force, RequestCache onlyif) 
+	{
+		requestEnums = new RequestCache[6];
+		requestEnums[0] = dflt;
+		requestEnums[1] = nostore;
+		requestEnums[2] = reload;
+		requestEnums[3] = nocache;
+		requestEnums[4] = force;
+		requestEnums[5] = onlyif;
+	}
+
+	public static void SetRequestEnumsProperties (JSObject obj) 
+	{
+		obj.SetObjectProperty("dflt", RequestCache.Default);
+		obj.SetObjectProperty("nostore", RequestCache.NoStore);
+		obj.SetObjectProperty("reload", RequestCache.Reload);
+		obj.SetObjectProperty("nocache", RequestCache.NoCache);
+		obj.SetObjectProperty("force", RequestCache.ForceCache);
+		obj.SetObjectProperty("onlyif", RequestCache.OnlyIfCached);
+	}
+
 }
 
 
@@ -299,6 +322,22 @@ public class FakeHttpClientHandler : HttpClientHandler
 	}
 }
 
+public enum RequestCache
+{
+	[Export(EnumValue = ConvertEnum.Default)]
+	Default = -1,
+	[Export("no-store")]
+	NoStore,
+	[Export(EnumValue = ConvertEnum.ToUpper)]
+	Reload,
+	[Export(EnumValue = ConvertEnum.ToLower)]
+	NoCache,
+	[Export("force-cache")]
+	ForceCache,
+	OnlyIfCached = -3636,
+}
+
+
 [TestFixture]
 public class BindingTests {
 	[Test]
@@ -963,4 +1002,38 @@ public class BindingTests {
 		Assert.AreNotEqual (null, TestClass.client);
 	}
 
+	[Test]
+	public static void MarshalRequestEnums () {
+		Runtime.InvokeJS (@"
+		    var dflt = ""Default"";
+			var nostore = ""no-store"";
+			var reload = ""RELOAD"";
+			var nocache = ""nocache"";
+			var force = 3;
+			var onlyif = -3636;
+			Module.mono_call_static_method (""[binding_tests]TestClass:SetRequestEnums"", [ dflt, nostore, reload, nocache, force, onlyif ]);
+		");
+		Assert.AreEqual (RequestCache.Default, TestClass.requestEnums[0]);
+		Assert.AreEqual (RequestCache.NoStore, TestClass.requestEnums[1]);
+		Assert.AreEqual (RequestCache.Reload, TestClass.requestEnums[2]);
+		Assert.AreEqual (RequestCache.NoCache, TestClass.requestEnums[3]);
+		Assert.AreEqual (RequestCache.ForceCache, TestClass.requestEnums[4]);
+		Assert.AreEqual (RequestCache.OnlyIfCached, TestClass.requestEnums[5]);
+	}
+
+	[Test]
+	public static void MarshalRequestEnumProps () {
+		Runtime.InvokeJS (@"
+		    var obj = {};
+			Module.mono_call_static_method (""[binding_tests]TestClass:SetRequestEnumsProperties"", [ obj ]);
+			Module.mono_call_static_method (""[binding_tests]TestClass:SetRequestEnums"", [ obj.dflt, obj.nostore, obj.reload, obj.nocache, obj.force, obj.onlyif ]);
+		");
+		Assert.AreEqual (RequestCache.Default, TestClass.requestEnums[0]);
+		Assert.AreEqual (RequestCache.NoStore, TestClass.requestEnums[1]);
+		Assert.AreEqual (RequestCache.Reload, TestClass.requestEnums[2]);
+		Assert.AreEqual (RequestCache.NoCache, TestClass.requestEnums[3]);
+		Assert.AreEqual (RequestCache.ForceCache, TestClass.requestEnums[4]);
+		Assert.AreEqual (RequestCache.OnlyIfCached, TestClass.requestEnums[5]);
+	}
+
 }

+ 183 - 2
sdks/wasm/bindings.cs

@@ -229,11 +229,19 @@ namespace WebAssembly
                     case TypeCode.Int32:
                     case TypeCode.UInt32:
                     case TypeCode.Boolean:
-                        res += "i";
+                        // Enums types have the same code as their underlying numeric types
+                        if (t.IsEnum)
+                            res += "j";
+                        else
+                            res += "i";
                         break;
                     case TypeCode.Int64:
                     case TypeCode.UInt64:
-                        res += "l";
+                        // Enums types have the same code as their underlying numeric types
+                        if (t.IsEnum)
+                            res += "k";
+                        else
+                            res += "l";
                         break;
                     case TypeCode.Single:
                         res += "f";
@@ -254,6 +262,21 @@ namespace WebAssembly
             return res;
         }
 
+        static object ObjectToEnum(IntPtr method_handle, int parm, object obj)
+        {
+            IntPtrAndHandle tmp = default(IntPtrAndHandle);
+            tmp.ptr = method_handle;
+
+            var mb = MethodBase.GetMethodFromHandle(tmp.handle);
+            var parmType = mb.GetParameters()[parm].ParameterType;
+            if (parmType.IsEnum)
+                return Runtime.EnumFromExportContract(parmType, obj);
+            else
+                return null;
+
+        }
+
+
         static MethodInfo gsjsc;
         static void GenericSetupJSContinuation<T>(Task<T> task, JSObject cont_obj)
         {
@@ -303,6 +326,127 @@ namespace WebAssembly
             return globalHandle;
         }
 
+        static string ObjectToString(object o)
+        {
+
+            if (o is Enum)
+                return EnumToExportContract((Enum)o).ToString();
+            
+            return o.ToString();
+        }        
+        // This is simple right now and will include FlagsAttribute later.
+        public static Enum EnumFromExportContract(Type enumType, object value)
+        {
+
+            if (!enumType.IsEnum)
+            {
+                throw new ArgumentException("Type provided must be an Enum.", nameof(enumType));
+            }
+
+            if (value is string)
+            {
+
+                var fields = enumType.GetFields();
+                foreach (var fi in fields)
+                {
+                    // Do not process special names
+                    if (fi.IsSpecialName)
+                        continue;
+
+                    ExportAttribute[] attributes =
+                        (ExportAttribute[])fi.GetCustomAttributes(typeof(ExportAttribute), false);
+
+                    var enumConversionType = ConvertEnum.Default;
+
+                    object contractName = null;
+
+                    if (attributes != null && attributes.Length > 0)
+                    {
+                        enumConversionType = attributes[0].EnumValue;
+                        if (enumConversionType != ConvertEnum.Numeric)
+                            contractName = attributes[0].ContractName;
+
+                    }
+
+                    if (contractName == null)
+                        contractName = fi.Name;
+
+                    switch (enumConversionType)
+                    {
+                        case ConvertEnum.ToLower:
+                            contractName = contractName.ToString().ToLower();
+                            break;
+                        case ConvertEnum.ToUpper:
+                            contractName = contractName.ToString().ToUpper();
+                            break;
+                        case ConvertEnum.Numeric:
+                            contractName = (int)Enum.Parse(value.GetType(), contractName.ToString());
+                            break;
+                        default:
+                            contractName = contractName.ToString();
+                            break;
+                    }
+
+                    if (contractName.ToString() == value.ToString())
+                    {
+                        return (Enum)Enum.Parse(enumType, fi.Name);
+                    }
+
+                }
+                 
+                throw new ArgumentException($"Value is a name, but not one of the named constants defined for the enum of type: {enumType}.", nameof(value));
+            }
+            else
+            {
+                return (Enum)Enum.ToObject(enumType, value);
+            }
+
+            return null;
+        }
+
+        // This is simple right now and will include FlagsAttribute later.
+        public static object EnumToExportContract(Enum value)
+        {
+
+            FieldInfo fi = value.GetType().GetField(value.ToString());
+
+            ExportAttribute[] attributes =
+                (ExportAttribute[])fi.GetCustomAttributes(typeof(ExportAttribute), false);
+
+            var enumConversionType = ConvertEnum.Default;
+
+            object contractName = null;
+
+            if (attributes != null && attributes.Length > 0)
+            {
+                enumConversionType = attributes[0].EnumValue;
+                if (enumConversionType != ConvertEnum.Numeric)
+                    contractName = attributes[0].ContractName;
+
+            }
+
+            if (contractName == null)
+                contractName = value;
+
+            switch (enumConversionType)
+            {
+                case ConvertEnum.ToLower:
+                    contractName = contractName.ToString().ToLower();
+                    break;
+                case ConvertEnum.ToUpper:
+                    contractName = contractName.ToString().ToUpper();
+                    break;
+                case ConvertEnum.Numeric:
+                    contractName = (int)Enum.Parse(value.GetType(), contractName.ToString());
+                    break;
+                default:
+                    contractName = contractName.ToString();
+                    break;
+            }
+
+            return contractName;
+        }
+
     }
 
     public class JSException : Exception
@@ -414,4 +558,41 @@ namespace WebAssembly
         }
 
     }
+
+    public enum ConvertEnum
+    {
+        Default,
+        ToLower,
+        ToUpper,
+        Numeric
+    }
+
+    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Field, 
+                    AllowMultiple = true, Inherited = false)]
+    public class ExportAttribute : Attribute
+    {
+        public ExportAttribute() : this(null, null)
+        {
+        }
+
+        public ExportAttribute(Type contractType) : this(null, contractType)
+        {
+        }
+
+        public ExportAttribute(string contractName) : this(contractName, null)
+        {
+        }
+
+        public ExportAttribute(string contractName, Type contractType)
+        {
+            ContractName = contractName;
+            ContractType = contractType;
+        }
+
+        public string ContractName { get; }
+
+        public Type ContractType { get; }
+        public ConvertEnum EnumValue { get; set; }
+    }
+
 }

+ 36 - 0
sdks/wasm/driver.c

@@ -147,6 +147,8 @@ MonoClass* mono_get_single_class (void);
 MonoClass* mono_get_double_class (void);
 MonoClass* mono_class_get_element_class(MonoClass *klass);
 int mono_regression_test_step (int verbose_level, char *image, char *method_name);
+int mono_class_is_enum (MonoClass *klass);
+MonoType* mono_type_get_underlying_type (MonoType *type);
 
 
 #define mono_array_get(array,type,index) ( *(type*)mono_array_addr ((array), type, (index)) ) 
@@ -377,6 +379,7 @@ class_is_task (MonoClass *klass)
 #define MARSHAL_TYPE_TASK 6
 #define MARSHAL_TYPE_OBJECT 7
 #define MARSHAL_TYPE_BOOL 8
+#define MARSHAL_TYPE_ENUM 9
 
 // typed array marshalling
 #define MARSHAL_ARRAY_BYTE 11
@@ -440,6 +443,8 @@ mono_wasm_get_obj_type (MonoObject *obj)
 		}		
 	}
 	default:
+		if (mono_class_is_enum (klass))
+			return MARSHAL_TYPE_ENUM;
 		if (!mono_type_is_reference (type)) //vt
 			return MARSHAL_TYPE_VT;
 		if (mono_class_is_delegate (klass))
@@ -594,3 +599,34 @@ mono_wasm_exec_regression (int verbose_level, char *image)
 {
 	return mono_regression_test_step (verbose_level, image, NULL) ? 0 : 1;
 }
+
+EMSCRIPTEN_KEEPALIVE int
+mono_wasm_unbox_enum (MonoObject *obj)
+{
+	if (!obj)
+		return 0;
+	
+	MonoType *type = mono_class_get_type (mono_object_get_class(obj));
+
+	void *ptr = mono_object_unbox (obj);
+	switch (mono_type_get_type(mono_type_get_underlying_type (type))) {
+	case MONO_TYPE_I1:
+	case MONO_TYPE_U1:
+		return *(unsigned char*)ptr;
+	case MONO_TYPE_I2:
+		return *(short*)ptr;
+	case MONO_TYPE_U2:
+		return *(unsigned short*)ptr;
+	case MONO_TYPE_I4:
+		return *(int*)ptr;
+	case MONO_TYPE_U4:
+		return *(unsigned int*)ptr;
+	// WASM doesn't support returning longs to JS
+	// case MONO_TYPE_I8:
+	// case MONO_TYPE_U8:
+	default:
+		printf ("Invalid type %d to mono_unbox_enum\n", mono_type_get_type(mono_type_get_underlying_type (type)));
+		return 0;
+	}
+}
+