[corlib] Import FileSystemInfo and family from CoreFX (#11342)
[mono-project.git] / sdks / wasm / bindings.cs
blob21a970ea8c062d59ec9ce0e64afc9b3b87fb595b
1 using System;
2 using System.Text;
3 using System.Collections.Generic;
4 using System.Threading;
5 using System.Threading.Tasks;
6 using System.Reflection;
7 using System.Runtime.InteropServices;
8 using System.Runtime.CompilerServices;
11 * TODO:
12 * - Expose annotated C# type to JS
13 * - Add property fetch to JSObject
14 * - Add typed method invoke support (get a delegate?)
15 * - Add JS helpers to fetch wrapped methods, like to Module.cwrap
16 * - Better Wrap C# exception when passing them as object (IE, on task failure)
17 * - Make JSObject disposable (same for js objects)
20 namespace WebAssembly
22 /// <summary>
23 /// Provides access to the Mono/WebAssembly runtime to perform tasks like invoking JavaScript functions and retrieving global variables.
24 /// </summary>
25 public sealed class Runtime
27 [MethodImplAttribute(MethodImplOptions.InternalCall)]
28 static extern string InvokeJS(string str, out int exceptional_result);
30 [MethodImplAttribute(MethodImplOptions.InternalCall)]
31 internal static extern object InvokeJSWithArgs(int js_obj_handle, string method, object[] _params, out int exceptional_result);
32 [MethodImplAttribute(MethodImplOptions.InternalCall)]
33 internal static extern object GetObjectProperty(int js_obj_handle, string propertyName, out int exceptional_result);
34 [MethodImplAttribute(MethodImplOptions.InternalCall)]
35 internal static extern object SetObjectProperty(int js_obj_handle, string propertyName, object value, bool createIfNotExists, bool hasOwnProperty, out int exceptional_result);
36 [MethodImplAttribute(MethodImplOptions.InternalCall)]
37 internal static extern object GetGlobalObject(string globalName, out int exceptional_result);
39 [MethodImplAttribute(MethodImplOptions.InternalCall)]
40 internal static extern object ReleaseHandle(int js_obj_handle, out int exceptional_result);
41 [MethodImplAttribute(MethodImplOptions.InternalCall)]
42 internal static extern object ReleaseObject(object obj, out int exceptional_result);
44 /// <summary>
45 /// Execute the provided string in the JavaScript context
46 public static string InvokeJS(string str)
48 int exception;
49 var res = InvokeJS(str, out exception);
50 if (exception != 0)
51 throw new JSException(res);
52 return res;
55 static Dictionary<int, JSObject> bound_objects = new Dictionary<int, JSObject>();
56 static Dictionary<object, JSObject> raw_to_js = new Dictionary<object, JSObject>();
58 static int BindJSObject(int js_id)
60 JSObject obj;
61 if (bound_objects.ContainsKey(js_id))
62 obj = bound_objects[js_id];
63 else
64 bound_objects[js_id] = obj = new JSObject(js_id);
66 return (int)(IntPtr)obj.Handle;
69 static int UnBindJSObject(int js_id)
71 if (bound_objects.ContainsKey(js_id))
73 var obj = bound_objects[js_id];
74 bound_objects.Remove(js_id);
75 return (int)(IntPtr)obj.Handle;
78 return 0;
82 static int BindJSObject(JSObject obj)
85 int js_id = obj.JSHandle;
86 if (js_id <= 0)
87 throw new JSException($"Invalid JS Object Handle {js_id}");
89 if (bound_objects.ContainsKey(js_id))
90 obj = bound_objects[js_id];
91 else
92 bound_objects[js_id] = obj;
94 return (int)(IntPtr)obj.Handle;
97 static int UnBindJSObjectAndFree(int js_id)
99 if (bound_objects.ContainsKey(js_id))
101 var obj = bound_objects[js_id];
102 bound_objects.Remove(js_id);
103 var gCHandle = obj.Handle;
104 obj.Handle.Free();
105 obj.JSHandle = -1;
106 obj.RawObject = null;
107 return (int)(IntPtr)gCHandle;
109 return 0;
114 static int UnBindRawJSObjectAndFree(int gcHandle)
117 GCHandle h = (GCHandle)(IntPtr)gcHandle;
118 JSObject obj = (JSObject)h.Target;
119 if (obj != null && obj.RawObject != null)
121 var raw_obj = obj.RawObject;
122 if (raw_to_js.ContainsKey(raw_obj))
124 raw_to_js.Remove(raw_obj);
127 obj.Dispose();
129 var gCHandle = obj.Handle;
130 obj.Handle.Free();
131 obj.JSHandle = -1;
132 return (int)(IntPtr)gCHandle;
134 return 0;
138 public static void FreeObject (object obj)
140 int exception;
141 Runtime.ReleaseObject(obj, out exception);
142 if (exception != 0)
143 throw new JSException($"Error releasing object on (raw-obj)");
147 static object CreateTaskSource(int js_id)
149 return new TaskCompletionSource<object>();
152 static void SetTaskSourceResult(TaskCompletionSource<object> tcs, object result)
154 tcs.SetResult(result);
157 static void SetTaskSourceFailure(TaskCompletionSource<object> tcs, string reason)
159 tcs.SetException(new JSException(reason));
162 static int GetTaskAndBind(TaskCompletionSource<object> tcs, int js_id)
164 return BindExistingObject(tcs.Task, js_id);
167 static int BindExistingObject(object raw_obj, int js_id)
170 JSObject obj;
171 if (raw_obj is JSObject)
172 obj = (JSObject)raw_obj;
173 else if (raw_to_js.ContainsKey(raw_obj))
174 obj = raw_to_js[raw_obj];
175 else
176 raw_to_js[raw_obj] = obj = new JSObject(js_id, raw_obj);
178 return (int)(IntPtr)obj.Handle;
181 static int GetJSObjectId(object raw_obj)
183 JSObject obj = null;
184 if (raw_obj is JSObject)
185 obj = (JSObject)raw_obj;
186 else if (raw_to_js.ContainsKey(raw_obj))
187 obj = raw_to_js[raw_obj];
189 var js_handle = obj != null ? obj.JSHandle : -1;
191 return js_handle;
194 static object GetMonoObject(int gc_handle)
196 GCHandle h = (GCHandle)(IntPtr)gc_handle;
197 JSObject o = (JSObject)h.Target;
198 if (o != null && o.RawObject != null)
199 return o.RawObject;
200 return o;
203 static object BoxInt(int i)
205 return i;
207 static object BoxDouble(double d)
209 return d;
212 static object BoxBool(int b)
214 return b == 0 ? false : true;
217 [StructLayout(LayoutKind.Explicit)]
218 internal struct IntPtrAndHandle
220 [FieldOffset(0)]
221 internal IntPtr ptr;
223 [FieldOffset(0)]
224 internal RuntimeMethodHandle handle;
227 //FIXME this probably won't handle generics
228 static string GetCallSignature(IntPtr method_handle)
230 IntPtrAndHandle tmp = default(IntPtrAndHandle);
231 tmp.ptr = method_handle;
233 var mb = MethodBase.GetMethodFromHandle(tmp.handle);
235 string res = "";
236 foreach (var p in mb.GetParameters())
238 var t = p.ParameterType;
240 switch (Type.GetTypeCode(t))
242 case TypeCode.Byte:
243 case TypeCode.SByte:
244 case TypeCode.Int16:
245 case TypeCode.UInt16:
246 case TypeCode.Int32:
247 case TypeCode.UInt32:
248 case TypeCode.Boolean:
249 // Enums types have the same code as their underlying numeric types
250 if (t.IsEnum)
251 res += "j";
252 else
253 res += "i";
254 break;
255 case TypeCode.Int64:
256 case TypeCode.UInt64:
257 // Enums types have the same code as their underlying numeric types
258 if (t.IsEnum)
259 res += "k";
260 else
261 res += "l";
262 break;
263 case TypeCode.Single:
264 res += "f";
265 break;
266 case TypeCode.Double:
267 res += "d";
268 break;
269 case TypeCode.String:
270 res += "s";
271 break;
272 default:
273 if (t.IsValueType)
274 throw new Exception("Can't handle VT arguments");
275 res += "o";
276 break;
279 return res;
282 static object ObjectToEnum(IntPtr method_handle, int parm, object obj)
284 IntPtrAndHandle tmp = default(IntPtrAndHandle);
285 tmp.ptr = method_handle;
287 var mb = MethodBase.GetMethodFromHandle(tmp.handle);
288 var parmType = mb.GetParameters()[parm].ParameterType;
289 if (parmType.IsEnum)
290 return Runtime.EnumFromExportContract(parmType, obj);
291 else
292 return null;
297 static MethodInfo gsjsc;
298 static void GenericSetupJSContinuation<T>(Task<T> task, JSObject cont_obj)
300 task.GetAwaiter().OnCompleted(() =>
303 if (task.Exception != null)
304 cont_obj.Invoke("reject", task.Exception.ToString());
305 else
307 cont_obj.Invoke("resolve", task.Result);
310 cont_obj.Dispose();
311 FreeObject(task);
316 static void SetupJSContinuation(Task task, JSObject cont_obj)
318 if (task.GetType() == typeof(Task))
320 task.GetAwaiter().OnCompleted(() =>
323 if (task.Exception != null)
324 cont_obj.Invoke("reject", task.Exception.ToString());
325 else
326 cont_obj.Invoke("resolve", null);
328 cont_obj.Dispose();
329 FreeObject(task);
332 else
334 //FIXME this is horrible codegen, we can do better with per-method glue
335 if (gsjsc == null)
336 gsjsc = typeof(Runtime).GetMethod("GenericSetupJSContinuation", BindingFlags.NonPublic | BindingFlags.Static);
337 gsjsc.MakeGenericMethod(task.GetType().GetGenericArguments()).Invoke(null, new object[] { task, cont_obj });
342 /// <summary>
343 /// Fetches a global object from the Javascript world, either from the current brower window or from the node.js global context.
344 /// </summary>
345 /// <remarks>
346 /// This method returns the value of a global object marshalled for consumption in C#.
347 /// </remarks>
348 /// <returns>
349 /// <para>
350 /// The return value can either be a primitive (string, int, double), a <see
351 /// cref="T:WebAssembly.JSObject"/> for JavaScript objects, a <see
352 /// cref="T:System.Threading.Tasks.Task">(object) for JavaScript promises, an array of
353 /// a byte, int or double (for Javascript objects typed as ArrayBuffer) or a <see
354 /// cref="T:System.Func"/> to represent JavaScript functions. The specific version of
355 /// the Func that will be returned depends on the parameters of the Javascript function
356 /// and return value.
357 /// </para>
358 /// <para>
359 /// The value of a returned promise (The Task(object) return) can in turn be any of the above
360 /// valuews.
361 /// </para>
362 /// </returns>
363 /// <param name="str">The name of the global object, or null if you want to retrieve the 'global' object itself.
364 /// On a browser, this is the 'window' object, on node.js it is the 'global' object.
365 /// </param>
366 public static object GetGlobalObject(string str = null)
368 int exception;
369 var globalHandle = Runtime.GetGlobalObject(str, out exception);
371 if (exception != 0)
372 throw new JSException($"Error obtaining a handle to global {str}");
374 return globalHandle;
377 static string ObjectToString(object o)
380 if (o is Enum)
381 return EnumToExportContract((Enum)o).ToString();
383 return o.ToString();
385 // This is simple right now and will include FlagsAttribute later.
386 public static Enum EnumFromExportContract(Type enumType, object value)
389 if (!enumType.IsEnum)
391 throw new ArgumentException("Type provided must be an Enum.", nameof(enumType));
394 if (value is string)
397 var fields = enumType.GetFields();
398 foreach (var fi in fields)
400 // Do not process special names
401 if (fi.IsSpecialName)
402 continue;
404 ExportAttribute[] attributes =
405 (ExportAttribute[])fi.GetCustomAttributes(typeof(ExportAttribute), false);
407 var enumConversionType = ConvertEnum.Default;
409 object contractName = null;
411 if (attributes != null && attributes.Length > 0)
413 enumConversionType = attributes[0].EnumValue;
414 if (enumConversionType != ConvertEnum.Numeric)
415 contractName = attributes[0].ContractName;
419 if (contractName == null)
420 contractName = fi.Name;
422 switch (enumConversionType)
424 case ConvertEnum.ToLower:
425 contractName = contractName.ToString().ToLower();
426 break;
427 case ConvertEnum.ToUpper:
428 contractName = contractName.ToString().ToUpper();
429 break;
430 case ConvertEnum.Numeric:
431 contractName = (int)Enum.Parse(value.GetType(), contractName.ToString());
432 break;
433 default:
434 contractName = contractName.ToString();
435 break;
438 if (contractName.ToString() == value.ToString())
440 return (Enum)Enum.Parse(enumType, fi.Name);
445 throw new ArgumentException($"Value is a name, but not one of the named constants defined for the enum of type: {enumType}.", nameof(value));
447 else
449 return (Enum)Enum.ToObject(enumType, value);
452 return null;
455 // This is simple right now and will include FlagsAttribute later.
456 public static object EnumToExportContract(Enum value)
459 FieldInfo fi = value.GetType().GetField(value.ToString());
461 ExportAttribute[] attributes =
462 (ExportAttribute[])fi.GetCustomAttributes(typeof(ExportAttribute), false);
464 var enumConversionType = ConvertEnum.Default;
466 object contractName = null;
468 if (attributes != null && attributes.Length > 0)
470 enumConversionType = attributes[0].EnumValue;
471 if (enumConversionType != ConvertEnum.Numeric)
472 contractName = attributes[0].ContractName;
476 if (contractName == null)
477 contractName = value;
479 switch (enumConversionType)
481 case ConvertEnum.ToLower:
482 contractName = contractName.ToString().ToLower();
483 break;
484 case ConvertEnum.ToUpper:
485 contractName = contractName.ToString().ToUpper();
486 break;
487 case ConvertEnum.Numeric:
488 contractName = (int)Enum.Parse(value.GetType(), contractName.ToString());
489 break;
490 default:
491 contractName = contractName.ToString();
492 break;
495 return contractName;
500 public class JSException : Exception
502 public JSException(string msg) : base(msg) { }
505 /// <summary>
506 /// JSObjects are wrappers for a native JavaScript object, and
507 /// they retain a reference to the JavaScript object for the lifetime of this C# object.
508 /// </summary>
509 public class JSObject : IDisposable
511 public int JSHandle { get; internal set; }
512 internal GCHandle Handle;
513 internal object RawObject;
515 internal JSObject(int js_handle)
517 this.JSHandle = js_handle;
518 this.Handle = GCHandle.Alloc(this);
521 internal JSObject(int js_id, object raw_obj)
523 this.JSHandle = js_id;
524 this.Handle = GCHandle.Alloc(this);
525 this.RawObject = raw_obj;
529 /// <returns>
530 /// <para>
531 /// The return value can either be a primitive (string, int, double), a <see
532 /// cref="T:WebAssembly.JSObject"/> for JavaScript objects, a <see
533 /// cref="T:System.Threading.Tasks.Task">(object) for JavaScript promises, an array of
534 /// a byte, int or double (for Javascript objects typed as ArrayBuffer) or a <see
535 /// cref="T:System.Func"/> to represent JavaScript functions. The specific version of
536 /// the Func that will be returned depends on the parameters of the Javascript function
537 /// and return value.
538 /// </para>
539 /// <para>
540 /// The value of a returned promise (The Task(object) return) can in turn be any of the above
541 /// valuews.
542 /// </para>
543 /// </returns>
544 public object Invoke(string method, params object[] args)
546 int exception;
547 var res = Runtime.InvokeJSWithArgs(JSHandle, method, args, out exception);
548 if (exception != 0)
549 throw new JSException((string)res);
550 return res;
553 /// <summary>
554 /// Returns the named property from the object, or throws a JSException on error.
555 /// </summary>
556 /// <param name="name">The name of the property to lookup</param>
557 /// <remarks>
558 /// This method can raise a <see cref="T:WebAssembly.JSException"/> if fetching the property in Javascript raises an exception.
559 /// </remarks>
560 /// <returns>
561 /// <para>
562 /// The return value can either be a primitive (string, int, double), a <see
563 /// cref="T:WebAssembly.JSObject"/> for JavaScript objects, a <see
564 /// cref="T:System.Threading.Tasks.Task">(object) for JavaScript promises, an array of
565 /// a byte, int or double (for Javascript objects typed as ArrayBuffer) or a <see
566 /// cref="T:System.Func"/> to represent JavaScript functions. The specific version of
567 /// the Func that will be returned depends on the parameters of the Javascript function
568 /// and return value.
569 /// </para>
570 /// <para>
571 /// The value of a returned promise (The Task(object) return) can in turn be any of the above
572 /// valuews.
573 /// </para>
574 /// </returns>
575 public object GetObjectProperty(string expr)
578 int exception;
579 var propertyValue = Runtime.GetObjectProperty(JSHandle, expr, out exception);
581 if (exception != 0)
582 throw new JSException((string)propertyValue);
584 return propertyValue;
588 /// <summary>
589 /// Sets the named property to the provided value.
590 /// </summary>
591 /// <remarks>
592 /// </remarks>
593 /// <param name="name">The name of the property to lookup</param>
594 /// <param name="value">The value can be a primitive type (int, double, string, bool), an
595 /// array that will be surfaced as a typed ArrayBuffer (byte[], sbyte[], short[], ushort[],
596 /// float[], double[]) </param>
597 /// <param name="createIfNotExists">Defaults to <see langword="true"/> and creates the property on the javascript object if not found, if set to <see langword="false"/> it will not create the property if it does not exist. If the property exists, the value is updated with the provided value.</param>
598 /// <param name="hasOwnProperty"></param>
599 public void SetObjectProperty(string expr, object value, bool createIfNotExists = true, bool hasOwnProperty = false)
602 int exception;
603 var setPropResult = Runtime.SetObjectProperty(JSHandle, expr, value, createIfNotExists, hasOwnProperty, out exception);
604 if (exception != 0)
605 throw new JSException($"Error setting {expr} on (js-obj js '{JSHandle}' mono '{(IntPtr)Handle} raw '{RawObject != null})");
609 protected void FreeHandle()
612 int exception;
613 Runtime.ReleaseHandle(JSHandle, out exception);
614 if (exception != 0)
615 throw new JSException($"Error releasing handle on (js-obj js '{JSHandle}' mono '{(IntPtr)Handle} raw '{RawObject != null})");
618 public override bool Equals(System.Object obj)
620 if (obj == null || GetType() != obj.GetType())
622 return false;
624 return JSHandle == (obj as JSObject).JSHandle;
627 public override int GetHashCode()
629 return JSHandle;
632 public void Dispose()
634 // Dispose of unmanaged resources.
635 Dispose(true);
636 // Suppress finalization.
637 GC.SuppressFinalize(this);
640 // Protected implementation of Dispose pattern.
641 protected virtual void Dispose(bool disposing)
643 if (JSHandle < 0)
644 return;
646 if (disposing)
649 // Free any other managed objects here.
653 // Free any unmanaged objects here.
655 FreeHandle();
658 public override string ToString()
660 return $"(js-obj js '{JSHandle}' mono '{(IntPtr)Handle} raw '{RawObject != null})";
665 public enum ConvertEnum
667 Default,
668 ToLower,
669 ToUpper,
670 Numeric
673 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Field,
674 AllowMultiple = true, Inherited = false)]
675 public class ExportAttribute : Attribute
677 public ExportAttribute() : this(null, null)
681 public ExportAttribute(Type contractType) : this(null, contractType)
685 public ExportAttribute(string contractName) : this(contractName, null)
689 public ExportAttribute(string contractName, Type contractType)
691 ContractName = contractName;
692 ContractType = contractType;
695 public string ContractName { get; }
697 public Type ContractType { get; }
698 public ConvertEnum EnumValue { get; set; }