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
;
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)
23 /// Provides access to the Mono/WebAssembly runtime to perform tasks like invoking JavaScript functions and retrieving global variables.
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
);
45 /// Execute the provided string in the JavaScript context
46 public static string InvokeJS(string str
)
49 var res
= InvokeJS(str
, out exception
);
51 throw new JSException(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
)
61 if (bound_objects
.ContainsKey(js_id
))
62 obj
= bound_objects
[js_id
];
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
;
82 static int BindJSObject(JSObject obj
)
85 int js_id
= obj
.JSHandle
;
87 throw new JSException($"Invalid JS Object Handle {js_id}");
89 if (bound_objects
.ContainsKey(js_id
))
90 obj
= bound_objects
[js_id
];
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
;
106 obj
.RawObject
= null;
107 return (int)(IntPtr
)gCHandle
;
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
);
129 var gCHandle
= obj
.Handle
;
132 return (int)(IntPtr
)gCHandle
;
138 public static void FreeObject (object obj
)
141 Runtime
.ReleaseObject(obj
, out exception
);
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
)
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
];
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
)
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;
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)
203 static object BoxInt(int i
)
207 static object BoxDouble(double d
)
212 static object BoxBool(int b
)
214 return b
== 0 ? false : true;
217 [StructLayout(LayoutKind
.Explicit
)]
218 internal struct IntPtrAndHandle
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
);
236 foreach (var p
in mb
.GetParameters())
238 var t
= p
.ParameterType
;
240 switch (Type
.GetTypeCode(t
))
245 case TypeCode
.UInt16
:
247 case TypeCode
.UInt32
:
248 case TypeCode
.Boolean
:
249 // Enums types have the same code as their underlying numeric types
256 case TypeCode
.UInt64
:
257 // Enums types have the same code as their underlying numeric types
263 case TypeCode
.Single
:
266 case TypeCode
.Double
:
269 case TypeCode
.String
:
274 throw new Exception("Can't handle VT arguments");
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
;
290 return Runtime
.EnumFromExportContract(parmType
, obj
);
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());
307 cont_obj
.Invoke("resolve", task
.Result
);
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());
326 cont_obj
.Invoke("resolve", null);
334 //FIXME this is horrible codegen, we can do better with per-method glue
336 gsjsc
= typeof(Runtime
).GetMethod("GenericSetupJSContinuation", BindingFlags
.NonPublic
| BindingFlags
.Static
);
337 gsjsc
.MakeGenericMethod(task
.GetType().GetGenericArguments()).Invoke(null, new object[] { task, cont_obj }
);
343 /// Fetches a global object from the Javascript world, either from the current brower window or from the node.js global context.
346 /// This method returns the value of a global object marshalled for consumption in C#.
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.
359 /// The value of a returned promise (The Task(object) return) can in turn be any of the above
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.
366 public static object GetGlobalObject(string str
= null)
369 var globalHandle
= Runtime
.GetGlobalObject(str
, out exception
);
372 throw new JSException($"Error obtaining a handle to global {str}");
377 static string ObjectToString(object o
)
381 return EnumToExportContract((Enum
)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
));
397 var fields
= enumType
.GetFields();
398 foreach (var fi
in fields
)
400 // Do not process special names
401 if (fi
.IsSpecialName
)
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();
427 case ConvertEnum
.ToUpper
:
428 contractName
= contractName
.ToString().ToUpper();
430 case ConvertEnum
.Numeric
:
431 contractName
= (int)Enum
.Parse(value.GetType(), contractName
.ToString());
434 contractName
= contractName
.ToString();
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));
449 return (Enum
)Enum
.ToObject(enumType
, value);
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();
484 case ConvertEnum
.ToUpper
:
485 contractName
= contractName
.ToString().ToUpper();
487 case ConvertEnum
.Numeric
:
488 contractName
= (int)Enum
.Parse(value.GetType(), contractName
.ToString());
491 contractName
= contractName
.ToString();
500 public class JSException
: Exception
502 public JSException(string msg
) : base(msg
) { }
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.
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
;
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.
540 /// The value of a returned promise (The Task(object) return) can in turn be any of the above
544 public object Invoke(string method
, params object[] args
)
547 var res
= Runtime
.InvokeJSWithArgs(JSHandle
, method
, args
, out exception
);
549 throw new JSException((string)res
);
554 /// Returns the named property from the object, or throws a JSException on error.
556 /// <param name="name">The name of the property to lookup</param>
558 /// This method can raise a <see cref="T:WebAssembly.JSException"/> if fetching the property in Javascript raises an exception.
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.
571 /// The value of a returned promise (The Task(object) return) can in turn be any of the above
575 public object GetObjectProperty(string expr
)
579 var propertyValue
= Runtime
.GetObjectProperty(JSHandle
, expr
, out exception
);
582 throw new JSException((string)propertyValue
);
584 return propertyValue
;
589 /// Sets the named property to the provided value.
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)
603 var setPropResult
= Runtime
.SetObjectProperty(JSHandle
, expr
, value, createIfNotExists
, hasOwnProperty
, out exception
);
605 throw new JSException($"Error setting {expr} on (js-obj js '{JSHandle}' mono '{(IntPtr)Handle} raw '{RawObject != null})");
609 protected void FreeHandle()
613 Runtime
.ReleaseHandle(JSHandle
, out exception
);
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())
624 return JSHandle
== (obj
as JSObject
).JSHandle
;
627 public override int GetHashCode()
632 public void Dispose()
634 // Dispose of unmanaged resources.
636 // Suppress finalization.
637 GC
.SuppressFinalize(this);
640 // Protected implementation of Dispose pattern.
641 protected virtual void Dispose(bool disposing
)
649 // Free any other managed objects here.
653 // Free any unmanaged objects here.
658 public override string ToString()
660 return $"(js-obj js '{JSHandle}' mono '{(IntPtr)Handle} raw '{RawObject != null})";
665 public enum ConvertEnum
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; }