/// <summary> /// Creates a <see cref="JavaScriptValue"/> representing the host delegate. /// </summary> /// <remarks>This call requires an active context.</remarks> public JavaScriptValue ToJsFunction(object obj, Type type) { var invokeInfo = type.GetMethod("Invoke"); if (null == invokeInfo) { throw new Exception($"Cannot convert delegate of type: {type} to JavaScript function."); } JavaScriptNativeFunction func = (v, s, args, argLength, data) => { var parameters = invokeInfo.GetParameters(); var realParams = new object[parameters.Length]; var argIndex = 1; for (int j = 0; j < parameters.Length; ++j) { var arg = args[argIndex++]; var param = parameters[j]; realParams[j] = ToHostObject(arg, param.ParameterType); } var result = invokeInfo.Invoke(obj, realParams); var resultType = invokeInfo.ReturnType; if (resultType == typeof(void)) { return(JavaScriptValue.Invalid); } return(ToJsObject(result, JsConversions.TypeFor(result, resultType))); }; return(_binder.BindFunction(func)); }
/// <summary> /// Binds all the fields for the instance. /// </summary> private void BindFields(JsBinding binding, IHostType hostType, object instance) { // TODO: Seems like we can hoist this into JsInterop, which would allow us refactor // TODO: out the builder class completely. var fields = hostType.FieldNames; for (int i = 0; i < fields.Count; ++i) { var fieldName = fields[i]; binding.AddProperty( fieldName, (v, s, args, argLength, data) => { var fieldInfo = hostType.FieldFor(fieldName).Field; var result = fieldInfo.GetValue(instance); var returnType = JsConversions.TypeFor(result, fieldInfo.FieldType); return(_interop.ToJsObject(result, returnType)); }, (v, s, args, argLength, data) => { var fieldInfo = hostType.FieldFor(fieldName).Field; var fieldType = fieldInfo.FieldType; var value = _interop.ToHostObject(args[1], fieldType); fieldInfo.SetValue(instance, value); return(JavaScriptValue.Invalid); }); } }
/// <summary> /// Binds all public properties for the instance. /// </summary> private void BindProperties(JsBinding binding, IHostType hostType, object instance) { // TODO: Seems like we can hoist this into JsInterop, which would allow us refactor // TODO: out the builder class completely. var properties = hostType.PropertyNames; for (int i = 0; i < properties.Count; ++i) { var propertyName = properties[i]; binding.AddProperty( propertyName, (v, s, args, argLength, data) => { var get = hostType.PropertyFor(propertyName).Getter; var result = get.Invoke(instance, EmptyParameters); var returnType = JsConversions.TypeFor(result, get.ReturnType); return(_interop.ToJsObject(result, returnType)); }, (v, s, args, argLength, data) => { var hostProperty = hostType.PropertyFor(propertyName); var propType = hostProperty.PropertyType; var set = hostProperty.Setter; var value = _interop.ToHostObject(args[1], propType); set.Invoke(instance, new[] { value }); return(JavaScriptValue.Invalid); }); } }
/// <summary> /// Converts a host object to a javascript value. /// </summary> public JavaScriptValue ToJsObject(object obj, Type type) { if (null == obj) { return(JavaScriptValue.Null); } if (JsConversions.IsVoidType(type)) { return(ToJsVoid(obj, type)); } if (JsConversions.IsNumberType(type)) { return(ToJsNumber(obj, type)); } if (JsConversions.IsStringType(type)) { return(ToJsString(obj, type)); } if (JsConversions.IsBoolType(type)) { return(ToJsBoolean(obj, type)); } if (JsConversions.IsFunctionType(type)) { return(ToJsFunction(obj, type)); } if (type.IsArray) { var underlyingType = type.GetElementType(); var hostArray = (Array)obj; var newArr = JavaScriptValue.CreateArray((uint)hostArray.Length); for (int i = 0; i < hostArray.Length; ++i) { var jsValue = ToJsObject(hostArray.GetValue(i), underlyingType); newArr.SetIndexedProperty(JavaScriptValue.FromInt32(i), jsValue); } return(newArr); } // Attempt to bind the object and return it return(ToBoundJsObject(obj, type)); }
/// <summary> /// This method type checks each parameter passed by javascript to determine if the method is a suitable /// match for execution. At this point, we can assume that the provided arguments qualify for the method /// signature in terms of number of parameters, accounting for optional parameters and var args. /// </summary> private bool IsCompatibleMethod(HostMethod method, JavaScriptValue[] args, ushort argLength) { var parameters = method.Parameters; var isVarArg = method.IsVarArgs; // Parameter Index Pointers - JS parameters include the callee at the first index, so we start after that var argIndex = 1; var pIndex = 0; // The maximum number of iterations we'll have to run to complete type checking each parameter // This is typically parameters.Length unless there are optional parameters or var args var iterations = Math.Max(parameters.Length, argLength - 1); // Loop Max Argument Count while (iterations-- > 0) { // Case 1. We've Type Checked All JS Parameters Against C# Parameters if (argIndex >= argLength) { // For a Var Arg C# Parameter Method, ensure we've indexed into the var arg index if (isVarArg) { // If we haven't, then we ensure that we have an optional parameter. Otherwise, // we're short JS parameters, so we can't call this method. if (pIndex != method.VarArgIndex) { return(parameters[pIndex].IsOptional); } return(true); } // For methods without var args, we simply ensure we've typed checked against all required // parameters. if (pIndex < parameters.Length) { return(parameters[pIndex].IsOptional); } return(true); } // Case 2. We reach the end of C# Method Parameters, but still have JS Parameters left to check if (pIndex >= parameters.Length) { // This case is only possible with var args (since the params []) counts as a single index if (!method.IsVarArgs) { return(false); } // Ensure that pIndex stays at the var arg index for type checking the element type pIndex = method.VarArgIndex; } // Case 3. TypeCheck JS Argument Against C# Parameter var arg = args[argIndex]; var parameter = parameters[pIndex]; // For Var Args, we need to ensure that we use the var arg type to check against the JS arg. var paramType = (isVarArg && pIndex >= method.VarArgIndex) ? method.VarArgType : parameter.ParameterType; // Increment index pointers argIndex++; pIndex++; // Run type conversion checking, early return on failure if (!JsConversions.IsAssignable(arg, _binder, paramType)) { return(false); } } return(true); }
/// <summary> /// Adds function bindings for all methods in the <see cref="IHostType"/>. /// </summary> private void BindMethods(JsBinding binding, IHostType hostType, object instance) { // TODO: Seems like we can hoist this into JsInterop, which would allow us refactor // TODO: out the builder class completely. var methods = hostType.MethodNames; for (int i = 0; i < methods.Count; ++i) { var methodName = methods[i]; binding.AddFunction( methodName, (v, s, args, argLength, data) => { var totalParameters = argLength - 1; var hostMethods = hostType.MethodsFor(methodName, totalParameters); if (hostMethods.Count == 0) { var message = $"Calling host function that does not exist: [Method: {methodName}, Instance: {instance}]"; JsErrorHelper.SetJsException(message); return(JavaScriptValue.Invalid); } HostMethod hostMethodInfo; // Only a single method returned. Call this optimistically if (hostMethods.Count == 1) { hostMethodInfo = hostMethods[0]; } else { // Get the Parameter Key, and look up cached invocation var invokeKey = JsConversions.ToInvokeKey(methodName, args, argLength); if (!hostType.TryGetInvocation(invokeKey, out hostMethodInfo)) { // Otherwise, locate best method for these argument, and cache hostMethodInfo = FindBestMethod(hostMethods, args, argLength); hostType.CacheInvocation(invokeKey, hostMethodInfo); } } if (null == hostMethodInfo) { LogMethodSelectionFailure(hostMethods, args, argLength); JsErrorHelper.SetJsException( $"Calling host function that does not exist: [Method: {methodName}, Instance: {instance}]"); return(JavaScriptValue.Invalid); } try { var realParams = ToParameters(hostMethodInfo, args, argLength); var result = hostMethodInfo.Method.Invoke(instance, realParams); var resultType = hostMethodInfo.ReturnType; if (resultType == typeof(void)) { return(JavaScriptValue.Invalid); } resultType = JsConversions.TypeFor(result, resultType); return(_interop.ToJsObject(result, resultType)); } catch (Exception e) { LogMethodInvocationInfo(hostMethodInfo, instance); throw; } }); } }