public XlParameterInfo ReturnType; // Macro will have ReturnType null // THROWS: Throws a DnaMarshalException if the method cannot be turned into an XlMethodInfo // TODO: Manage errors if things go wrong private XlMethodInfo(MethodInfo targetMethod, ModuleBuilder modBuilder) { // Default Name, Description and Category Name = targetMethod.Name; Description = ""; Category = IntegrationHelpers.DnaLibraryGetName(); HelpTopic = ""; IsVolatile = false; IsExceptionSafe = false; IsHidden = false; IsMacroType = false; IsThreadSafe = false; ShortCut = ""; // DOCUMENT: Default MenuName is the library name // but menu is only added if at least the MenuText is set. MenuName = IntegrationHelpers.DnaLibraryGetName(); MenuText = null; // Menu is only SetAttributeInfo(targetMethod.GetCustomAttributes(false)); // Return type conversion if (targetMethod.ReturnType == typeof(void)) { IsCommand = true; ReturnType = null; // HACK: Check that there are no other parameters // Does Excel allow macros with parameters? if (targetMethod.GetParameters().Length > 0) { throw new DnaMarshalException("Macros with parameters are not supported."); } } else { IsCommand = false; ReturnType = new XlParameterInfo(targetMethod.ReturnType, true, IsExceptionSafe); } // Parameters - meta-data and type conversion Parameters = Array.ConvertAll <ParameterInfo, XlParameterInfo>( targetMethod.GetParameters(), delegate(ParameterInfo pi) { return(new XlParameterInfo(pi)); }); // Create the delegate type, wrap the targetMethod and create the delegate Type delegateType = CreateDelegateType(modBuilder); Delegate xlDelegate = CreateMethodDelegate(targetMethod, delegateType); // Need to add a reference to prevent garbage collection of our delegate // Don't need to pin, according to // "How to: Marshal Callbacks and Delegates Using C++ Interop" // Currently this delegate is never released // TODO: Clean up properly DelegateHandle = GCHandle.Alloc(xlDelegate); FunctionPointer = Marshal.GetFunctionPointerForDelegate(xlDelegate); }
private static object[] GetRegisterParameters(XlMethodInfo mi, string exportedProcName) { string functionType; if (mi.ReturnType != null) { functionType = mi.ReturnType.XlType; } else { if (mi.Parameters.Length == 0) { functionType = ""; // OK since no other types will be added } else { // This case is also be used for native async functions functionType = ">"; // Use the void / inplace indicator if needed. } } string argumentNames = ""; bool showDescriptions = false; // For async functions, we need to leave off the last argument int numArgumentDescriptions = mi.IsExcelAsyncFunction ? mi.Parameters.Length - 1 : mi.Parameters.Length; string[] argumentDescriptions = new string[numArgumentDescriptions]; for (int j = 0; j < numArgumentDescriptions; j++) { XlParameterInfo pi = mi.Parameters[j]; functionType += pi.XlType; if (j > 0) { argumentNames += ","; // TODO: Should this be a comma, or the Excel list separator? } argumentNames += pi.Name; argumentDescriptions[j] = pi.Description; if (pi.Description != "") { showDescriptions = true; } // DOCUMENT: Truncate the argument description if it exceeds the Excel limit of 255 characters if (j < mi.Parameters.Length - 1) { if (!string.IsNullOrEmpty(argumentDescriptions[j]) && argumentDescriptions[j].Length > 255) { argumentDescriptions[j] = argumentDescriptions[j].Substring(0, 255); Logger.Registration.Warn("Truncated argument description of '{0}' in function '{1}'", pi.Name, mi.Name); } } else { // Last argument - need to deal with extra ". " if (!string.IsNullOrEmpty(argumentDescriptions[j])) { if (argumentDescriptions[j].Length > 253) { argumentDescriptions[j] = argumentDescriptions[j].Substring(0, 253); Logger.Registration.Warn("Truncated final argument description of function '{0}'", mi.Name); } // DOCUMENT: Here is the patch for the Excel Function Description bug. // DOCUMENT: Ensure the ". " suffix exists on the last parameter. if (!argumentDescriptions[j].EndsWith(". ")) { argumentDescriptions[j] += argumentDescriptions[j].EndsWith(".") ? " " : ". "; } } } } // for each parameter // Add async handle if (mi.IsExcelAsyncFunction) { functionType += "X"; // mi.Parameters[mi.Parameters.Length - 1].XlType should be "X" anyway } // Native async functions cannot be cluster safe if (mi.IsClusterSafe && ProcessHelper.SupportsClusterSafe && !mi.IsExcelAsyncFunction) { functionType += "&"; } if (mi.IsMacroType) { functionType += "#"; } if (!mi.IsMacroType && mi.IsThreadSafe && XlAddIn.XlCallVersion >= 12) { functionType += "$"; } if (mi.IsVolatile) { functionType += "!"; } // DOCUMENT: If # is set and there is an R argument, Excel considers the function volatile anyway. // You can call xlfVolatile, false in beginning of function to clear. // DOCUMENT: Truncate Description to 253 characters (for all versions) string functionDescription = Truncate(mi.Description, 253, "function description", mi); // DOCUMENT: Here is the patch for the Excel Function Description bug. // DOCUMENT: Ensure the ". " suffix exists if the function takes no parameters and has a description. if (mi.Parameters.Length == 0 && functionDescription != "" && !functionDescription.EndsWith(". ")) { functionDescription += functionDescription.EndsWith(".") ? " " : ". "; } // DOCUMENT: When there is no description, we don't add any. // This allows the user to work around the Excel bug where an extra parameter is displayed if // the function has no parameter but displays a description if (mi.Description != "") { showDescriptions = true; } int numRegisterParameters; // DOCUMENT: Maximum 20 Argument Descriptions when registering using Excel4 function. int maxDescriptions = (XlAddIn.XlCallVersion < 12) ? 20 : 245; if (showDescriptions) { numArgumentDescriptions = Math.Min(numArgumentDescriptions, maxDescriptions); numRegisterParameters = 10 + numArgumentDescriptions; // function description + arg descriptions } else { // Won't be showing any descriptions. numArgumentDescriptions = 0; numRegisterParameters = 9; } // DOCUMENT: Additional truncations of registration info - registration fails with strings longer than 255 chars. argumentNames = Truncate(argumentNames, 255, "argument names", mi); argumentNames = argumentNames.TrimEnd(','); // Also trim trailing commas (for params case) string category = Truncate(mi.Category, 255, "Category name", mi); string name = Truncate(mi.Name, 255, "Name", mi); string helpTopic = string.Empty; if (mi.HelpTopic != null) { if (mi.HelpTopic.Length > 255) { // Can't safely truncate the help link Logger.Registration.Warn("Ignoring HelpTopic of function '{0}' - too long", mi.Name); } else { // It's OK helpTopic = mi.HelpTopic; } } object[] registerParameters = new object[numRegisterParameters]; registerParameters[0] = XlAddIn.PathXll; registerParameters[1] = exportedProcName; registerParameters[2] = functionType; registerParameters[3] = name; registerParameters[4] = argumentNames; registerParameters[5] = mi.IsCommand ? 2 /*macro*/ : (mi.IsHidden ? 0 : 1); /*function*/ registerParameters[6] = category; registerParameters[7] = mi.ShortCut; /*shortcut_text*/ registerParameters[8] = helpTopic; /*help_topic*/ if (showDescriptions) { registerParameters[9] = functionDescription; for (int k = 0; k < numArgumentDescriptions; k++) { registerParameters[10 + k] = argumentDescriptions[k]; } } return(registerParameters); }
public XlParameterInfo ReturnType; // Macro will have ReturnType null (as will native async functions) // THROWS: Throws a DnaMarshalException if the method cannot be turned into an XlMethodInfo // TODO: Manage errors if things go wrong XlMethodInfo(ModuleBuilder modBuilder, MethodInfo targetMethod, object target, object methodAttribute, List<object> argumentAttributes) { // Default Name, Description and Category Name = targetMethod.Name; Description = ""; Category = IntegrationHelpers.DnaLibraryGetName(); HelpTopic = ""; IsVolatile = false; IsExceptionSafe = false; IsHidden = false; IsMacroType = false; IsThreadSafe = false; IsClusterSafe = false; ExplicitRegistration = false; ShortCut = ""; // DOCUMENT: Default MenuName is the library name // but menu is only added if at least the MenuText is set. MenuName = IntegrationHelpers.DnaLibraryGetName(); MenuText = null; // Menu is only // Set default IsCommand - overridden by having an [ExcelCommand] attribute, // or by being a native async function. // (Must be done before SetAttributeInfo) IsCommand = (targetMethod.ReturnType == typeof(void)); SetAttributeInfo(methodAttribute); // We shortcut the rest of the registration if (ExplicitRegistration) return; FixHelpTopic(); // Return type conversion // Careful here - native async functions also return void if (targetMethod.ReturnType == typeof(void)) { ReturnType = null; } else { ReturnType = new XlParameterInfo(targetMethod.ReturnType, true, IsExceptionSafe); } ParameterInfo[] parameters = targetMethod.GetParameters(); // Parameters - meta-data and type conversion Parameters = new XlParameterInfo[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { object argAttrib = null; if ( argumentAttributes != null && i < argumentAttributes.Count) argAttrib = argumentAttributes[i]; Parameters[i] = new XlParameterInfo(parameters[i], argAttrib); } // A native async function might still be marked as a command - check and fix. // (these have the ExcelAsyncHandle as last parameter) // (This check needs the Parameters array to be set up already.) if (IsExcelAsyncFunction) { // It really is a function, though it might return null IsCommand = false; } // Create the delegate type, wrap the targetMethod and create the delegate // CONSIDER: Currently we need a special delegate type here so that we can hook on the marshaling attributes. // Future version might do straight-forward marshaling, so we can get rid of these types (just use generic methods) // FirstArgument (if received) is not used in the delegate type created ... Type delegateType = CreateDelegateType(modBuilder); // ... but is baked into the delegate itself. Delegate xlDelegate = CreateMethodDelegate(delegateType, targetMethod, target); // Need to add a reference to prevent garbage collection of our delegate // Don't need to pin, according to // "How to: Marshal Callbacks and Delegates Using C++ Interop" // Currently this delegate is never released // TODO: Clean up properly DelegateHandle = GCHandle.Alloc(xlDelegate); FunctionPointer = Marshal.GetFunctionPointerForDelegate(xlDelegate); }
private Delegate CreateMethodDelegate(MethodInfo targetMethod, Type delegateType) { // Check whether we can skip wrapper if (IsExceptionSafe && Array.TrueForAll(Parameters, delegate(XlParameterInfo pi){ return(pi.BoxedValueType == null); }) && (IsCommand || ReturnType.BoxedValueType == null)) { // Create the delegate directly return(Delegate.CreateDelegate(delegateType, targetMethod)); } // Else we create a dynamic wrapper Type[] paramTypes = Array.ConvertAll <XlParameterInfo, Type>(Parameters, delegate(XlParameterInfo pi) { return(pi.DelegateParamType); }); DynamicMethod wrapper = new DynamicMethod( string.Format("Wrapped_f{0}_{1}", Index, targetMethod.Name), IsCommand ? typeof(void) : ReturnType.DelegateParamType, paramTypes, typeof(object), true); ILGenerator wrapIL = wrapper.GetILGenerator(); Label endOfMethod = wrapIL.DefineLabel(); LocalBuilder retobj = null; if (!IsCommand) { // Make a local to contain the return value retobj = wrapIL.DeclareLocal(ReturnType.DelegateParamType); } if (!IsExceptionSafe) { // Start the Try block wrapIL.BeginExceptionBlock(); } // Generate the body // push all the arguments for (byte i = 0; i < paramTypes.Length; i++) { wrapIL.Emit(OpCodes.Ldarg_S, i); XlParameterInfo pi = Parameters[i]; if (pi.BoxedValueType != null) { wrapIL.Emit(OpCodes.Unbox_Any, pi.BoxedValueType); } } // Call the real method wrapIL.EmitCall(OpCodes.Call, targetMethod, null); if (!IsCommand && ReturnType.BoxedValueType != null) { // Box the return value (which is on the stack) wrapIL.Emit(OpCodes.Box, ReturnType.BoxedValueType); } if (!IsCommand) { // Store the return value into the local variable wrapIL.Emit(OpCodes.Stloc_S, retobj); } if (!IsExceptionSafe) { wrapIL.Emit(OpCodes.Leave_S, endOfMethod); wrapIL.BeginCatchBlock(typeof(object)); wrapIL.Emit(OpCodes.Pop); if (!IsCommand && ReturnType.DelegateParamType == typeof(object)) { // Create a boxed Excel error value, and set the return object to it wrapIL.Emit(OpCodes.Ldc_I4, IntegrationMarshalHelpers.ExcelError_ExcelErrorValue); wrapIL.Emit(OpCodes.Box, IntegrationMarshalHelpers.GetExcelErrorType()); wrapIL.Emit(OpCodes.Stloc_S, retobj); } wrapIL.EndExceptionBlock(); } wrapIL.MarkLabel(endOfMethod); if (!IsCommand) { // Push the return value wrapIL.Emit(OpCodes.Ldloc_S, retobj); } wrapIL.Emit(OpCodes.Ret); return(wrapper.CreateDelegate(delegateType));; }
Delegate CreateMethodDelegate(Type delegateType, MethodInfo targetMethod, object target) { bool isInstanceMethod = !targetMethod.IsStatic; // We expect static methods to have target null Debug.Assert(isInstanceMethod || target == null); // Check whether we can skip wrapper completely // TODO: Change this - we should always wrap in an exception handler, but for double return values // we let IsExceptionSafe mean we do a 1/0 in the exception handler, which Excel will catch and handle as #NUM! // For other types it shouldn't matter... if (IsExceptionSafe && Array.TrueForAll(Parameters, delegate(XlParameterInfo pi) { return(pi.BoxedValueType == null); }) && (!HasReturnType || ReturnType.BoxedValueType == null)) { // Create the delegate directly if (isInstanceMethod) { // Can't be DynamicMethod return(Delegate.CreateDelegate(delegateType, target, targetMethod)); } if (targetMethod is DynamicMethod) { return(((DynamicMethod)targetMethod).CreateDelegate(delegateType)); } return(Delegate.CreateDelegate(delegateType, targetMethod)); } // DateTime input parameters are never exception safe - we need to be able to fail out of the // marshaler when the passed-in argument is an invalid date. bool emitExceptionHandler = !IsExceptionSafe || Array.Exists(Parameters, delegate(XlParameterInfo pi) { return(pi.BoxedValueType == typeof(DateTime)); }); // Now we create a dynamic wrapper Type[] paramTypes = Array.ConvertAll <XlParameterInfo, Type>(Parameters, delegate(XlParameterInfo pi) { return(pi.DelegateParamType); }); if (isInstanceMethod) { Type[] allParams = new Type[paramTypes.Length + 1]; allParams[0] = typeof(object); Array.Copy(paramTypes, 0, allParams, 1, paramTypes.Length); paramTypes = allParams; } DynamicMethod wrapper = new DynamicMethod( string.Format("Wrapped_f{0}_{1}", Index, targetMethod.Name), HasReturnType ? ReturnType.DelegateParamType : typeof(void), paramTypes, typeof(object), true); ILGenerator wrapIL = wrapper.GetILGenerator(); Label endOfMethod = wrapIL.DefineLabel(); LocalBuilder retobj = null; if (HasReturnType) { // Make a local to contain the return value retobj = wrapIL.DeclareLocal(ReturnType.DelegateParamType); } if (emitExceptionHandler) { // Start the Try block wrapIL.BeginExceptionBlock(); } // Generate the body - push all the arguments, including the target for instance methods if (isInstanceMethod) { // First is the target of the delegate wrapIL.Emit(OpCodes.Ldarg_S, 0); } for (byte i = 0; i < Parameters.Length; i++) { if (i < 255) { byte argIndex = isInstanceMethod ? (byte)(i + 1) : i; wrapIL.Emit(OpCodes.Ldarg_S, argIndex); } else { short argIndex = isInstanceMethod ? (short)(i + 1) : i; wrapIL.Emit(OpCodes.Ldarg, argIndex); } XlParameterInfo pi = Parameters[i]; if (pi.BoxedValueType != null) { wrapIL.Emit(OpCodes.Unbox_Any, pi.BoxedValueType); } } // Call the real method wrapIL.EmitCall(OpCodes.Call, targetMethod, null); if (HasReturnType) { if (ReturnType.BoxedValueType != null) { // Box the return value (which is on the stack) wrapIL.Emit(OpCodes.Box, ReturnType.BoxedValueType); } // Store the return value into the local variable wrapIL.Emit(OpCodes.Stloc_S, retobj); } if (emitExceptionHandler) { wrapIL.Emit(OpCodes.Leave_S, endOfMethod); wrapIL.BeginCatchBlock(typeof(object)); if (HasReturnType && ReturnType.DelegateParamType == typeof(object)) { // Call Integration.HandleUnhandledException - Exception object is on the stack. wrapIL.EmitCall(OpCodes.Call, IntegrationHelpers.UnhandledExceptionHandler, null); // Stack now has return value from the ExceptionHandler - Store to local wrapIL.Emit(OpCodes.Stloc_S, retobj); //// Create a boxed Excel error value, and set the return object to it //wrapIL.Emit(OpCodes.Ldc_I4, IntegrationMarshalHelpers.ExcelError_ExcelErrorValue); //wrapIL.Emit(OpCodes.Box, IntegrationMarshalHelpers.GetExcelErrorType()); //wrapIL.Emit(OpCodes.Stloc_S, retobj); } else { // Just ignore the Exception. wrapIL.Emit(OpCodes.Pop); } wrapIL.EndExceptionBlock(); } wrapIL.MarkLabel(endOfMethod); if (HasReturnType) { // Push the return value wrapIL.Emit(OpCodes.Ldloc_S, retobj); } wrapIL.Emit(OpCodes.Ret); // End of Wrapper if (isInstanceMethod) { return(wrapper.CreateDelegate(delegateType, target)); } return(wrapper.CreateDelegate(delegateType)); }
public XlParameterInfo ReturnType; // Macro will have ReturnType null (as will native async functions) // THROWS: Throws a DnaMarshalException if the method cannot be turned into an XlMethodInfo // TODO: Manage errors if things go wrong XlMethodInfo(ModuleBuilder modBuilder, MethodInfo targetMethod, object target, object methodAttribute, List <object> argumentAttributes) { // Default Name, Description and Category Name = targetMethod.Name; Description = ""; Category = IntegrationHelpers.DnaLibraryGetName(); HelpTopic = ""; IsVolatile = false; IsExceptionSafe = false; IsHidden = false; IsMacroType = false; IsThreadSafe = false; IsClusterSafe = false; ExplicitRegistration = false; ShortCut = ""; // DOCUMENT: Default MenuName is the library name // but menu is only added if at least the MenuText is set. MenuName = IntegrationHelpers.DnaLibraryGetName(); MenuText = null; // Menu is only // Set default IsCommand - overridden by having an [ExcelCommand] attribute, // or by being a native async function. // (Must be done before SetAttributeInfo) IsCommand = (targetMethod.ReturnType == typeof(void)); SetAttributeInfo(methodAttribute); // We shortcut the rest of the registration if (ExplicitRegistration) { return; } FixHelpTopic(); // Return type conversion // Careful here - native async functions also return void if (targetMethod.ReturnType == typeof(void)) { ReturnType = null; } else { ReturnType = new XlParameterInfo(targetMethod.ReturnType, true, IsExceptionSafe); } ParameterInfo[] parameters = targetMethod.GetParameters(); // Parameters - meta-data and type conversion Parameters = new XlParameterInfo[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { object argAttrib = null; if (argumentAttributes != null && i < argumentAttributes.Count) { argAttrib = argumentAttributes[i]; } Parameters[i] = new XlParameterInfo(parameters[i], argAttrib); } // A native async function might still be marked as a command - check and fix. // (these have the ExcelAsyncHandle as last parameter) // (This check needs the Parameters array to be set up already.) if (IsExcelAsyncFunction) { // It really is a function, though it might return null IsCommand = false; } // Create the delegate type, wrap the targetMethod and create the delegate // CONSIDER: Currently we need a special delegate type here so that we can hook on the marshaling attributes. // Future version might do straight-forward marshaling, so we can get rid of these types (just use generic methods) // FirstArgument (if received) is not used in the delegate type created ... Type delegateType = CreateDelegateType(modBuilder); // ... but is baked into the delegate itself. Delegate xlDelegate = CreateMethodDelegate(delegateType, targetMethod, target); // Need to add a reference to prevent garbage collection of our delegate // Don't need to pin, according to // "How to: Marshal Callbacks and Delegates Using C++ Interop" // Currently this delegate is never released // TODO: Clean up properly DelegateHandle = GCHandle.Alloc(xlDelegate); FunctionPointer = Marshal.GetFunctionPointerForDelegate(xlDelegate); }
// THROWS: Throws a DnaMarshalException if the method cannot be turned into an XlMethodInfo // TODO: Manage errors if things go wrong XlMethodInfo(MethodInfo targetMethod, object target, object methodAttribute, List <object> argumentAttributes) { // Default Name, Description and Category Name = targetMethod.Name; Description = ""; Category = IntegrationHelpers.DnaLibraryGetName(); HelpTopic = ""; IsVolatile = false; IsExceptionSafe = false; IsHidden = false; IsMacroType = false; IsThreadSafe = false; IsClusterSafe = false; ExplicitRegistration = false; ShortCut = ""; // DOCUMENT: Default MenuName is the library name // but menu is only added if at least the MenuText is set. MenuName = IntegrationHelpers.DnaLibraryGetName(); MenuText = null; // Menu is only // Set default IsCommand - overridden by having an [ExcelCommand] attribute, // or by being a native async function. // (Must be done before SetAttributeInfo) IsCommand = (targetMethod.ReturnType == typeof(void)); SetAttributeInfo(methodAttribute); // We shortcut the rest of the registration if (ExplicitRegistration) { return; } FixHelpTopic(); // Return type conversion // Careful here - native async functions also return void if (targetMethod.ReturnType == typeof(void)) { ReturnType = null; } else { ReturnType = new XlParameterInfo(targetMethod.ReturnType, true, IsExceptionSafe); } ParameterInfo[] parameters = targetMethod.GetParameters(); // Parameters - meta-data and type conversion Parameters = new XlParameterInfo[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { object argAttrib = null; if (argumentAttributes != null && i < argumentAttributes.Count) { argAttrib = argumentAttributes[i]; } Parameters[i] = new XlParameterInfo(parameters[i], argAttrib); } // A native async function might still be marked as a command - check and fix. // (these have the ExcelAsyncHandle as last parameter) // (This check needs the Parameters array to be set up already.) if (IsExcelAsyncFunction) { // It really is a function, though it might return null IsCommand = false; } }
private static void RegisterXlMethod(XlMethodInfo mi) { int index = registeredMethods.Count; SetJump(index, mi.FunctionPointer); String procName = String.Format("f{0}", index); string functionType = mi.ReturnType == null ? "" : mi.ReturnType.XlType.ToString(); string argumentNames = ""; bool showDescriptions = false; string[] argumentDescriptions = new string[mi.Parameters.Length]; string helpTopic; for (int j = 0; j < mi.Parameters.Length; j++) { XlParameterInfo pi = mi.Parameters[j]; functionType += pi.XlType; if (j > 0) { argumentNames += ","; } argumentNames += pi.Name; argumentDescriptions[j] = pi.Description; if (pi.Description != "") { showDescriptions = true; } // DOCUMENT: Here is the patch for the Excel Function Description bug. // DOCUMENT: I add ". " to the last parameters. if (j == mi.Parameters.Length - 1) { argumentDescriptions[j] += ". "; } } // for each parameter if (mi.IsMacroType) { functionType += "#"; } else if (mi.IsThreadSafe && XlAddIn.xlCallVersion >= 12) { functionType += "$"; } if (mi.IsVolatile) { functionType += "!"; } // DOCUMENT: If # is set and there is an R argument, // Excel considers the function volatile // You can call xlfVolatile, false in beginning of function to clear. // DOCUMENT: Here is the patch for the Excel Function Description bug. // DOCUMENT: I add ". " if the function takes no parameters and has a description. string functionDescription = mi.Description; if (mi.Parameters.Length == 0 && functionDescription != "") { functionDescription += ". "; } // DOCUMENT: When there is no description, we don't add any. // This allows the user to work around the Excel bug where an extra parameter is displayed if // the function has no parameter but displays a description if (mi.Description != "") { showDescriptions = true; } int numArguments; // DOCUMENT: Maximum 20 Argument Descriptions when registering using Excel4 function. int maxDescriptions = (XlAddIn.xlCallVersion < 12) ? 20 : 245; int numArgumentDescriptions; if (showDescriptions) { numArgumentDescriptions = Math.Min(argumentDescriptions.Length, maxDescriptions); numArguments = 10 + numArgumentDescriptions; } else { numArgumentDescriptions = 0; numArguments = 9; } // Make HelpTopic without full path relative to xllPath if (mi.HelpTopic == null || mi.HelpTopic == "") { helpTopic = mi.HelpTopic; } else { if (Path.IsPathRooted(mi.HelpTopic)) { helpTopic = mi.HelpTopic; } else { helpTopic = Path.Combine(Path.GetDirectoryName(pathXll), mi.HelpTopic); } } object[] registerParameters = new object[numArguments]; registerParameters[0] = pathXll; registerParameters[1] = procName; registerParameters[2] = functionType; registerParameters[3] = mi.Name; registerParameters[4] = argumentNames; registerParameters[5] = mi.IsCommand ? 2 /*macro*/ : (mi.IsHidden ? 0 : 1); /*function*/ registerParameters[6] = mi.Category; registerParameters[7] = mi.ShortCut; /*shortcut_text*/ registerParameters[8] = helpTopic; /*help_topic*/; if (showDescriptions) { registerParameters[9] = functionDescription; for (int k = 0; k < numArgumentDescriptions; k++) { registerParameters[10 + k] = argumentDescriptions[k]; } } // Basically suppress problems here !? try { object xlCallResult; XlCallImpl.TryExcelImpl(XlCallImpl.xlfRegister, out xlCallResult, registerParameters); mi.RegisterId = (double)xlCallResult; registeredMethods.Add(mi); } catch (Exception e) { // TODO: What to do here? Debug.WriteLine(e.Message); } if (mi.IsCommand) { RegisterMenu(mi); } }
MethodInfo CreateMethodInfo(TypeBuilder wrapperType, Type delegateType, MethodInfo targetMethod, object target) { bool isInstanceMethod = !targetMethod.IsStatic; // We expect static methods to have target null Debug.Assert(isInstanceMethod || target == null); // Check whether we can skip wrapper completely // TODO: Change this - we should always wrap in an exception handler, but for double return values // we let IsExceptionSafe mean we do a 1/0 in the exception handler, which Excel will catch and handle as #NUM! // For other types it shouldn't matter... if (IsExceptionSafe && Array.TrueForAll(Parameters, delegate(XlParameterInfo pi) { return(pi.BoxedValueType == null); }) && (!HasReturnType || ReturnType.BoxedValueType == null)) { return(targetMethod); } // DateTime and boolean input parameters are never exception safe - we need to be able to fail out of the // marshaler when the passed-in argument is an invalid date or bool. bool emitExceptionHandler = !IsExceptionSafe || Array.Exists(Parameters, delegate(XlParameterInfo pi) { return(pi.BoxedValueType == typeof(DateTime) || pi.BoxedValueType == typeof(bool)); }); if (emitExceptionHandler && IsExceptionSafe && HasReturnType && ReturnType.DelegateParamType != typeof(object)) { throw new DnaMarshalException("DateTime and bool input parameters are incompatible with IsExceptionSafe attribute"); } // Now we create a dynamic wrapper Type[] paramTypes = Array.ConvertAll <XlParameterInfo, Type>(Parameters, delegate(XlParameterInfo pi) { return(pi.DelegateParamType); }); if (isInstanceMethod) { Type[] allParams = new Type[paramTypes.Length + 1]; allParams[0] = typeof(object); Array.Copy(paramTypes, 0, allParams, 1, paramTypes.Length); paramTypes = allParams; } MethodBuilder wrapper = wrapperType.DefineMethod( string.Format("Wrapped_f{0}_{1}", Index, targetMethod.Name), MethodAttributes.Public | MethodAttributes.Static, HasReturnType ? ReturnType.DelegateParamType : typeof(void), paramTypes); // Add [HandleCorruptProcessExceptions] to wrapper method, if we're running on .NET 4.0 if (Environment.Version.Major >= 4 && emitExceptionHandler) { Type hcpeType = Type.GetType("System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptionsAttribute"); ConstructorInfo hcpeCtorInfo = hcpeType.GetConstructor(Type.EmptyTypes); CustomAttributeBuilder hcpeBuilder = new CustomAttributeBuilder(hcpeCtorInfo, Type.EmptyTypes); wrapper.SetCustomAttribute(hcpeBuilder); } ILGenerator wrapIL = wrapper.GetILGenerator(); Label endOfMethod = wrapIL.DefineLabel(); LocalBuilder retobj = null; LocalBuilder except = null; if (HasReturnType) { // Make a local to contain the return value retobj = wrapIL.DeclareLocal(ReturnType.DelegateParamType); } if (emitExceptionHandler) { // Start the Try block except = wrapIL.DeclareLocal(typeof(Exception)); wrapIL.BeginExceptionBlock(); } // Generate the body - push all the arguments, including the target for instance methods if (isInstanceMethod) { // First is the target of the delegate wrapIL.Emit(OpCodes.Ldarg_S, 0); } for (byte i = 0; i < Parameters.Length; i++) { if (i < 255) { byte argIndex = isInstanceMethod ? (byte)(i + 1) : i; wrapIL.Emit(OpCodes.Ldarg_S, argIndex); } else { short argIndex = isInstanceMethod ? (short)(i + 1) : i; wrapIL.Emit(OpCodes.Ldarg, argIndex); } XlParameterInfo pi = Parameters[i]; if (pi.BoxedValueType != null) { wrapIL.Emit(OpCodes.Unbox_Any, pi.BoxedValueType); } } // Call the real method wrapIL.EmitCall(OpCodes.Call, targetMethod, null); if (HasReturnType) { if (ReturnType.BoxedValueType != null) { // Box the return value (which is on the stack) wrapIL.Emit(OpCodes.Box, ReturnType.BoxedValueType); } // Store the return value into the local variable wrapIL.Emit(OpCodes.Stloc_S, retobj); } if (emitExceptionHandler) { wrapIL.Emit(OpCodes.Leave_S, endOfMethod); wrapIL.BeginCatchBlock(typeof(object)); if (IsExcelAsyncFunction) { wrapIL.Emit(OpCodes.Stloc_S, except); int idx = Parameters.Length - (isInstanceMethod ? 0 : 1); if (idx < 256) { wrapIL.Emit(OpCodes.Ldarg_S, (byte)idx); } else { wrapIL.Emit(OpCodes.Ldarg, (short)idx); } wrapIL.Emit(OpCodes.Ldloc_S, except); wrapIL.Emit(OpCodes.Callvirt, IntegrationMarshalHelpers.ExcelAsyncHandleType.GetMethod("SetException")); wrapIL.Emit(OpCodes.Pop); } else if (!HasReturnType || ReturnType.DelegateParamType == typeof(object)) { // Call Integration.HandleUnhandledException - Exception object is on the stack. wrapIL.Emit(OpCodes.Call, typeof(IntegrationHelpers).GetMethod("HandleUnhandledException")); if (HasReturnType) { // Stack now has return value from the ExceptionHandler - Store to local if we have a return type wrapIL.Emit(OpCodes.Stloc_S, retobj); //// Create a boxed Excel error value, and set the return object to it //wrapIL.Emit(OpCodes.Ldc_I4, IntegrationMarshalHelpers.ExcelError_ExcelErrorValue); //wrapIL.Emit(OpCodes.Box, IntegrationMarshalHelpers.GetExcelErrorType()); //wrapIL.Emit(OpCodes.Stloc_S, retobj); } else { // Pop result from ExceptionHandler off the stack wrapIL.Emit(OpCodes.Pop); } } else { // Just ignore the Exception. wrapIL.Emit(OpCodes.Pop); } wrapIL.EndExceptionBlock(); } wrapIL.MarkLabel(endOfMethod); if (HasReturnType) { // Push the return value wrapIL.Emit(OpCodes.Ldloc_S, retobj); } wrapIL.Emit(OpCodes.Ret); // End of Wrapper return(wrapper); }
private static void RegisterXlMethod(XlMethodInfo mi) { int index = registeredMethods.Count; SetJump(index, mi.FunctionPointer); String procName = String.Format("f{0}", index); string functionType; if (mi.IsCommand) { if (mi.Parameters.Length == 0) { functionType = ""; // OK since no other types will be added } else { functionType = ">"; // Use the void / inplace indicator if needed. } } else { functionType = mi.ReturnType.XlType; } string argumentNames = ""; bool showDescriptions = false; string[] argumentDescriptions = new string[mi.Parameters.Length]; string helpTopic; for (int j = 0; j < mi.Parameters.Length; j++) { XlParameterInfo pi = mi.Parameters[j]; functionType += pi.XlType; if (j > 0) { argumentNames += ","; } argumentNames += pi.Name; argumentDescriptions[j] = pi.Description; if (pi.Description != "") { showDescriptions = true; } // DOCUMENT: Truncate the argument description if it exceeds the Excel limit of 255 characters if (j < mi.Parameters.Length - 1) { if (!string.IsNullOrEmpty(argumentDescriptions[j]) && argumentDescriptions[j].Length > 255) { argumentDescriptions[j] = argumentDescriptions[j].Substring(0, 255); Debug.Print("Truncated argument description of {0} in method {1} as Excel limit was exceeded", pi.Name, mi.Name); } } else { // Last argument - need to deal with extra ". " if (!string.IsNullOrEmpty(argumentDescriptions[j])) { if (argumentDescriptions[j].Length > 253) { argumentDescriptions[j] = argumentDescriptions[j].Substring(0, 253); Debug.Print("Truncated field description of {0} in method {1} as Excel limit was exceeded", pi.Name, mi.Name); } // DOCUMENT: Here is the patch for the Excel Function Description bug. // DOCUMENT: I add ". " to the last parameter. argumentDescriptions[j] += ". "; } } } // for each parameter if (mi.IsClusterSafe && ProcessHelper.SupportsClusterSafe) { functionType += "&"; } if (mi.IsMacroType) { functionType += "#"; } if (!mi.IsMacroType && mi.IsThreadSafe && XlAddIn.XlCallVersion >= 12) { functionType += "$"; } if (mi.IsVolatile) { functionType += "!"; } // DOCUMENT: If # is set and there is an R argument, Excel considers the function volatile anyway. // You can call xlfVolatile, false in beginning of function to clear. // DOCUMENT: There is a bug? in Excel 2007 that limits the total argumentname string to 255 chars. // TODO: Check whether this is fixed in Excel 2010 yet. // DOCUMENT: I truncate the argument string for all versions. if (argumentNames.Length > 255) { argumentNames = argumentNames.Substring(0, 255); } // DOCUMENT: Here is the patch for the Excel Function Description bug. // DOCUMENT: I add ". " if the function takes no parameters and has a description. string functionDescription = mi.Description; if (mi.Parameters.Length == 0 && functionDescription != "") { functionDescription += ". "; } // DOCUMENT: When there is no description, we don't add any. // This allows the user to work around the Excel bug where an extra parameter is displayed if // the function has no parameter but displays a description if (mi.Description != "") { showDescriptions = true; } int numArguments; // DOCUMENT: Maximum 20 Argument Descriptions when registering using Excel4 function. int maxDescriptions = (XlAddIn.XlCallVersion < 12) ? 20 : 245; int numArgumentDescriptions; if (showDescriptions) { numArgumentDescriptions = Math.Min(argumentDescriptions.Length, maxDescriptions); numArguments = 10 + numArgumentDescriptions; } else { numArgumentDescriptions = 0; numArguments = 9; } // Make HelpTopic without full path relative to xllPath if (string.IsNullOrEmpty(mi.HelpTopic)) { helpTopic = mi.HelpTopic; } else { // DOCUMENT: If HelpTopic is not rooted - it is expanded relative to .xll path. // If http url does not end with !0 it is appended. // I don't think https is supported, but it should not be considered an 'unrooted' path anyway. if (mi.HelpTopic.StartsWith("http://") || mi.HelpTopic.StartsWith("https://")) { if (!mi.HelpTopic.EndsWith("!0")) { helpTopic = mi.HelpTopic + "!0"; } else { helpTopic = mi.HelpTopic; } } else if (Path.IsPathRooted(mi.HelpTopic)) { helpTopic = mi.HelpTopic; } else { helpTopic = Path.Combine(Path.GetDirectoryName(pathXll), mi.HelpTopic); } } object[] registerParameters = new object[numArguments]; registerParameters[0] = pathXll; registerParameters[1] = procName; registerParameters[2] = functionType; registerParameters[3] = mi.Name; registerParameters[4] = argumentNames; registerParameters[5] = mi.IsCommand ? 2 /*macro*/ : (mi.IsHidden ? 0 : 1); /*function*/ registerParameters[6] = mi.Category; registerParameters[7] = mi.ShortCut; /*shortcut_text*/ registerParameters[8] = helpTopic; /*help_topic*/; if (showDescriptions) { registerParameters[9] = functionDescription; for (int k = 0; k < numArgumentDescriptions; k++) { registerParameters[10 + k] = argumentDescriptions[k]; } } // Basically suppress problems here !? try { object xlCallResult; XlCallImpl.TryExcelImpl(XlCallImpl.xlfRegister, out xlCallResult, registerParameters); Debug.Print("Register - XllPath={0}, ProcName={1}, FunctionType={2}, MethodName={3} - Result={4}", registerParameters[0], registerParameters[1], registerParameters[2], registerParameters[3], xlCallResult); if (xlCallResult is double) { mi.RegisterId = (double)xlCallResult; registeredMethods.Add(mi); if (mi.IsCommand) { RegisterMenu(mi); RegisterShortCut(mi); } } else { // TODO: What to do here? LogDisplay?? Debug.Print("Registration Error! - Register call failed for method {0}", mi.Name); } } catch (Exception e) { // TODO: What to do here? LogDisplay?? Debug.WriteLine("Registration Error! - " + e.Message); } }
// THROWS: Throws a DnaMarshalException if the method cannot be turned into an XlMethodInfo // TODO: Manage errors if things go wrong XlMethodInfo(MethodInfo targetMethod, object target, object methodAttribute, List<object> argumentAttributes) { // Default Name, Description and Category Name = targetMethod.Name; Description = ""; Category = IntegrationHelpers.DnaLibraryGetName(); HelpTopic = ""; IsVolatile = false; IsExceptionSafe = false; IsHidden = false; IsMacroType = false; IsThreadSafe = false; IsClusterSafe = false; ExplicitRegistration = false; ShortCut = ""; // DOCUMENT: Default MenuName is the library name // but menu is only added if at least the MenuText is set. MenuName = IntegrationHelpers.DnaLibraryGetName(); MenuText = null; // Menu is only // Set default IsCommand - overridden by having an [ExcelCommand] attribute, // or by being a native async function. // (Must be done before SetAttributeInfo) IsCommand = (targetMethod.ReturnType == typeof(void)); SetAttributeInfo(methodAttribute); // We shortcut the rest of the registration if (ExplicitRegistration) return; FixHelpTopic(); // Return type conversion // Careful here - native async functions also return void if (targetMethod.ReturnType == typeof(void)) { ReturnType = null; } else { ReturnType = new XlParameterInfo(targetMethod.ReturnType, true, IsExceptionSafe); } ParameterInfo[] parameters = targetMethod.GetParameters(); // Parameters - meta-data and type conversion Parameters = new XlParameterInfo[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { object argAttrib = null; if ( argumentAttributes != null && i < argumentAttributes.Count) argAttrib = argumentAttributes[i]; Parameters[i] = new XlParameterInfo(parameters[i], argAttrib); } // A native async function might still be marked as a command - check and fix. // (these have the ExcelAsyncHandle as last parameter) // (This check needs the Parameters array to be set up already.) if (IsExcelAsyncFunction) { // It really is a function, though it might return null IsCommand = false; } }
private static object[] GetRegisterParameters(XlMethodInfo mi, string exportedProcName) { string functionType; if (mi.ReturnType != null) { functionType = mi.ReturnType.XlType; } else { if (mi.Parameters.Length == 0) { functionType = ""; // OK since no other types will be added } else { // This case is also be used for native async functions functionType = ">"; // Use the void / inplace indicator if needed. } } // TODO: The argument names and descriptions allow some undocumented ",..." form to support paramarray style functions. // E.g. check the FuncSum function in Generic.c in the SDK. // We should try some support for this... string argumentNames = ""; bool showDescriptions = false; // For async functions, we need to leave off the last argument int numArgumentDescriptions = mi.IsExcelAsyncFunction ? mi.Parameters.Length - 1 : mi.Parameters.Length; string[] argumentDescriptions = new string[numArgumentDescriptions]; for (int j = 0; j < numArgumentDescriptions; j++) { XlParameterInfo pi = mi.Parameters[j]; functionType += pi.XlType; if (j > 0) { argumentNames += ","; } argumentNames += pi.Name; argumentDescriptions[j] = pi.Description; if (pi.Description != "") { showDescriptions = true; } // DOCUMENT: Truncate the argument description if it exceeds the Excel limit of 255 characters if (j < mi.Parameters.Length - 1) { if (!string.IsNullOrEmpty(argumentDescriptions[j]) && argumentDescriptions[j].Length > 255) { argumentDescriptions[j] = argumentDescriptions[j].Substring(0, 255); Debug.Print("Truncated argument description of {0} in method {1} as Excel limit was exceeded", pi.Name, mi.Name); } } else { // Last argument - need to deal with extra ". " if (!string.IsNullOrEmpty(argumentDescriptions[j])) { if (argumentDescriptions[j].Length > 253) { argumentDescriptions[j] = argumentDescriptions[j].Substring(0, 253); Debug.Print("Truncated field description of {0} in method {1} as Excel limit was exceeded", pi.Name, mi.Name); } // DOCUMENT: Here is the patch for the Excel Function Description bug. // DOCUMENT: I add ". " to the last parameter. argumentDescriptions[j] += ". "; } } } // for each parameter // Add async handle if (mi.IsExcelAsyncFunction) { functionType += "X"; // mi.Parameters[mi.Parameters.Length - 1].XlType should be "X" anyway } // Native async functions cannot be cluster safe if (mi.IsClusterSafe && ProcessHelper.SupportsClusterSafe && !mi.IsExcelAsyncFunction) { functionType += "&"; } if (mi.IsMacroType) { functionType += "#"; } if (!mi.IsMacroType && mi.IsThreadSafe && XlAddIn.XlCallVersion >= 12) { functionType += "$"; } if (mi.IsVolatile) { functionType += "!"; } // DOCUMENT: If # is set and there is an R argument, Excel considers the function volatile anyway. // You can call xlfVolatile, false in beginning of function to clear. string functionDescription = mi.Description; // DOCUMENT: Truncate Description to 253 characters (for all versions) functionDescription = Truncate(functionDescription, 253); // DOCUMENT: Here is the patch for the Excel Function Description bug. // DOCUMENT: I add ". " if the function takes no parameters and has a description. if (mi.Parameters.Length == 0 && functionDescription != "") { functionDescription += ". "; } // DOCUMENT: When there is no description, we don't add any. // This allows the user to work around the Excel bug where an extra parameter is displayed if // the function has no parameter but displays a description if (mi.Description != "") { showDescriptions = true; } int numRegisterParameters; // DOCUMENT: Maximum 20 Argument Descriptions when registering using Excel4 function. int maxDescriptions = (XlAddIn.XlCallVersion < 12) ? 20 : 245; if (showDescriptions) { numArgumentDescriptions = Math.Min(numArgumentDescriptions, maxDescriptions); numRegisterParameters = 10 + numArgumentDescriptions; // function description + arg descriptions } else { // Won't be showing any descriptions. numArgumentDescriptions = 0; numRegisterParameters = 9; } // DOCUMENT: Additional truncations of registration info - registration fails with strings longer than 255 chars. argumentNames = Truncate(argumentNames, 255); string category = Truncate(mi.Category, 255); string name = Truncate(mi.Name, 255); string helpTopic = (mi.HelpTopic == null || mi.HelpTopic.Length <= 255) ? mi.HelpTopic : ""; object[] registerParameters = new object[numRegisterParameters]; registerParameters[0] = XlAddIn.PathXll; registerParameters[1] = exportedProcName; registerParameters[2] = functionType; registerParameters[3] = name; registerParameters[4] = argumentNames; registerParameters[5] = mi.IsCommand ? 2 /*macro*/ : (mi.IsHidden ? 0 : 1); /*function*/ registerParameters[6] = category; registerParameters[7] = mi.ShortCut; /*shortcut_text*/ registerParameters[8] = helpTopic; /*help_topic*/ if (showDescriptions) { registerParameters[9] = functionDescription; for (int k = 0; k < numArgumentDescriptions; k++) { registerParameters[10 + k] = argumentDescriptions[k]; } } return(registerParameters); }
public string ShortCut; // For macros only #endregion Fields #region Constructors // THROWS: Throws a DnaMarshalException if the method cannot be turned into an XlMethodInfo // TODO: Manage errors if things go wrong XlMethodInfo(MethodInfo targetMethod, ModuleBuilder modBuilder) { // Default Name, Description and Category Name = targetMethod.Name; Description = ""; Category = IntegrationHelpers.DnaLibraryGetName(); HelpTopic = ""; IsVolatile = false; IsExceptionSafe = false; IsHidden = false; IsMacroType = false; IsThreadSafe = false; IsClusterSafe = false; ShortCut = ""; // DOCUMENT: Default MenuName is the library name // but menu is only added if at least the MenuText is set. MenuName = IntegrationHelpers.DnaLibraryGetName(); MenuText = null; // Menu is only SetAttributeInfo(targetMethod.GetCustomAttributes(false)); // Return type conversion if (targetMethod.ReturnType == typeof (void)) { IsCommand = true; ReturnType = null; } else { IsCommand = false; ReturnType = new XlParameterInfo(targetMethod.ReturnType, true, IsExceptionSafe); } // Parameters - meta-data and type conversion Parameters = Array.ConvertAll<ParameterInfo, XlParameterInfo>( targetMethod.GetParameters(), delegate(ParameterInfo pi) { return new XlParameterInfo(pi); }); // Create the delegate type, wrap the targetMethod and create the delegate Type delegateType = CreateDelegateType(modBuilder); Delegate xlDelegate = CreateMethodDelegate(targetMethod, delegateType); // Need to add a reference to prevent garbage collection of our delegate // Don't need to pin, according to // "How to: Marshal Callbacks and Delegates Using C++ Interop" // Currently this delegate is never released // TODO: Clean up properly DelegateHandle = GCHandle.Alloc(xlDelegate); FunctionPointer = Marshal.GetFunctionPointerForDelegate(xlDelegate); }