1 /** 2 This module implements the compile-time reflection machinery to 3 automatically register all D functions that are eligible in a 4 compile-time define list of modules to be called from Excel. 5 6 Import this module from any module from your XLL build and: 7 8 ----------- 9 import xlld; 10 11 mixin(implGetWorksheetFunctionsString!("module1", "module2", "module3")); 12 ----------- 13 14 All eligible functions in the 3 example modules above will automagically 15 be accessible from Excel (assuming the built XLL is loaded as an add-in). 16 */ 17 module xlld.traits; 18 19 import xlld.worksheet; 20 import xlld.xlcall; 21 import std.traits: isSomeFunction, allSatisfy, isSomeString; 22 23 // import unit_threaded and introduce helper functions for testing 24 version(unittest) { 25 import unit_threaded; 26 27 // return a WorksheetFunction for a double function(double) with no 28 // optional arguments 29 WorksheetFunction makeWorksheetFunction(wstring name, wstring typeText) @safe pure nothrow { 30 return 31 WorksheetFunction( 32 Procedure(name), 33 TypeText(typeText), 34 FunctionText(name), 35 Optional( 36 ArgumentText(""w), 37 MacroType("1"w), 38 Category(""w), 39 ShortcutText(""w), 40 HelpTopic(""w), 41 FunctionHelp(""w), 42 ArgumentHelp([]), 43 ) 44 ); 45 } 46 47 WorksheetFunction doubleToDoubleFunction(wstring name) @safe pure nothrow { 48 return makeWorksheetFunction(name, "BB"w); 49 } 50 51 WorksheetFunction FP12ToDoubleFunction(wstring name) @safe pure nothrow { 52 return makeWorksheetFunction(name, "BK%"w); 53 } 54 55 WorksheetFunction operToOperFunction(wstring name) @safe pure nothrow { 56 return makeWorksheetFunction(name, "UU"w); 57 } 58 } 59 60 /** 61 Take a D function as a compile-time parameter and returns a 62 WorksheetFunction struct with the fields filled in accordingly. 63 */ 64 WorksheetFunction getWorksheetFunction(alias F)() if(isSomeFunction!F) { 65 import std.traits: ReturnType, Parameters, getUDAs; 66 import std.conv: text; 67 68 alias R = ReturnType!F; 69 alias T = Parameters!F; 70 71 static if(!isWorksheetFunction!F) { 72 throw new Exception("Unsupported function type " ~ R.stringof ~ T.stringof ~ " for " ~ 73 __traits(identifier, F).stringof[1 .. $-1]); 74 } else { 75 76 WorksheetFunction ret; 77 ret.procedure = Procedure(__traits(identifier, F)); 78 ret.functionText = FunctionText(__traits(identifier, F)); 79 ret.typeText = TypeText(getTypeText!F); 80 81 // check to see if decorated with @Register 82 alias registerAttrs = getUDAs!(F, Register); 83 static if(registerAttrs.length > 0) { 84 static assert(registerAttrs.length == 1, 85 text("Only 1 @Register allowed, found ", registerAttrs.length, 86 " on function ", __traits(identifier, F))); 87 ret.optional = registerAttrs[0]; 88 } 89 90 return ret; 91 } 92 } 93 94 @("getWorksheetFunction for double -> double functions with no extra attributes") 95 @safe pure unittest { 96 double foo(double) nothrow @nogc { return 0; } 97 getWorksheetFunction!foo.shouldEqual(doubleToDoubleFunction("foo")); 98 99 double bar(double) nothrow @nogc { return 0; } 100 getWorksheetFunction!bar.shouldEqual(doubleToDoubleFunction("bar")); 101 } 102 103 @("getWorksheetFunction for double -> int functions should fail") 104 @safe pure unittest { 105 double foo(int) { return 0; } 106 getWorksheetFunction!foo.shouldThrowWithMessage("Unsupported function type double(int) for foo"); 107 } 108 109 @("getworksheetFunction with @Register in order") 110 @safe pure unittest { 111 112 @Register(ArgumentText("my arg txt"), MacroType("macro")) 113 double foo(double) nothrow; 114 115 auto expected = doubleToDoubleFunction("foo"); 116 expected.argumentText = ArgumentText("my arg txt"); 117 expected.macroType = MacroType("macro"); 118 119 getWorksheetFunction!foo.shouldEqual(expected); 120 } 121 122 @("getworksheetFunction with @Register out of order") 123 @safe pure unittest { 124 125 @Register(HelpTopic("I need somebody"), ArgumentText("my arg txt")) 126 double foo(double) nothrow; 127 128 auto expected = doubleToDoubleFunction("foo"); 129 expected.argumentText = ArgumentText("my arg txt"); 130 expected.helpTopic = HelpTopic("I need somebody"); 131 132 getWorksheetFunction!foo.shouldEqual(expected); 133 } 134 135 136 private wstring getTypeText(alias F)() if(isSomeFunction!F) { 137 import std.traits: ReturnType, Parameters; 138 139 wstring typeToString(T)() { 140 static if(is(T == double)) 141 return "B"; 142 else static if(is(T == FP12*)) 143 return "K%"; 144 else static if(is(T == LPXLOPER12)) 145 return "U"; 146 else 147 static assert(false, "Unsupported type " ~ T.stringof); 148 } 149 150 auto retType = typeToString!(ReturnType!F); 151 foreach(argType; Parameters!F) 152 retType ~= typeToString!(argType); 153 154 return retType; 155 } 156 157 158 @("getTypeText") 159 @safe pure unittest { 160 import std.conv: to; // working around unit-threaded bug 161 162 double foo(double); 163 getTypeText!foo.to!string.shouldEqual("BB"); 164 165 double bar(FP12*); 166 getTypeText!bar.to!string.shouldEqual("BK%"); 167 168 FP12* baz(FP12*); 169 getTypeText!baz.to!string.shouldEqual("K%K%"); 170 171 FP12* qux(double); 172 getTypeText!qux.to!string.shouldEqual("K%B"); 173 174 LPXLOPER12 fun(LPXLOPER12); 175 getTypeText!fun.to!string.shouldEqual("UU"); 176 } 177 178 179 180 // helper template for aliasing 181 private alias Identity(alias T) = T; 182 183 184 // whether or not this is a function that has the "right" types 185 template isSupportedFunction(alias F, T...) { 186 import std.traits: isSomeFunction, ReturnType, Parameters, functionAttributes, FunctionAttribute; 187 import std.meta: AliasSeq, allSatisfy; 188 import std.typecons: Tuple; 189 190 // trying to get a pointer to something is a good way of making sure we can 191 // attempt to evaluate `isSomeFunction` - it's not always possible 192 enum canGetPointerToIt = __traits(compiles, &F); 193 enum isOneOfSupported(U) = isSupportedType!(U, T); 194 195 static if(canGetPointerToIt) { 196 static if(isSomeFunction!F) { 197 198 enum isSupportedFunction = 199 __traits(compiles, F(Tuple!(Parameters!F)().expand)) && 200 isOneOfSupported!(ReturnType!F) && 201 allSatisfy!(isOneOfSupported, Parameters!F) && 202 functionAttributes!F & FunctionAttribute.nothrow_; 203 204 static if(!isSupportedFunction && !(functionAttributes!F & FunctionAttribute.nothrow_)) 205 pragma(msg, "Warning: Function '", __traits(identifier, F), "' not considered because it throws"); 206 207 } else 208 enum isSupportedFunction = false; 209 } else 210 enum isSupportedFunction = false; 211 } 212 213 214 // if T is one of U 215 private template isSupportedType(T, U...) { 216 static if(U.length == 0) 217 enum isSupportedType = false; 218 else 219 enum isSupportedType = is(T == U[0]) || isSupportedType!(T, U[1..$]); 220 } 221 222 @safe pure unittest { 223 static assert(isSupportedType!(int, int, int)); 224 static assert(!isSupportedType!(int, double, string)); 225 } 226 227 // whether or not this is a function that can be called from Excel 228 private enum isWorksheetFunction(alias F) = isSupportedFunction!(F, double, FP12*, LPXLOPER12); 229 230 @safe pure unittest { 231 double doubleToDouble(double) nothrow; 232 static assert(isWorksheetFunction!doubleToDouble); 233 234 LPXLOPER12 operToOper(LPXLOPER12) nothrow; 235 static assert(isWorksheetFunction!operToOper); 236 } 237 238 239 /** 240 Gets all Excel-callable functions in a given module 241 */ 242 WorksheetFunction[] getModuleWorksheetFunctions(string moduleName)() { 243 mixin(`import ` ~ moduleName ~ `;`); 244 alias module_ = Identity!(mixin(moduleName)); 245 246 WorksheetFunction[] ret; 247 248 foreach(moduleMemberStr; __traits(allMembers, module_)) { 249 250 alias moduleMember = Identity!(__traits(getMember, module_, moduleMemberStr)); 251 252 static if(isWorksheetFunction!moduleMember) { 253 try 254 ret ~= getWorksheetFunction!moduleMember; 255 catch(Exception ex) 256 assert(0); //can't happen 257 } 258 } 259 260 return ret; 261 } 262 263 @("getWorksheetFunctions on test_xl_funcs") 264 @safe pure unittest { 265 getModuleWorksheetFunctions!"xlld.test_xl_funcs".shouldEqual( 266 [ 267 doubleToDoubleFunction("FuncMulByTwo"), 268 FP12ToDoubleFunction("FuncFP12"), 269 operToOperFunction("FuncFib"), 270 ] 271 ); 272 } 273 274 /** 275 Gets all Excel-callable functions from the given modules 276 */ 277 WorksheetFunction[] getAllWorksheetFunctions(Modules...)() pure @safe if(allSatisfy!(isSomeString, typeof(Modules))) { 278 WorksheetFunction[] ret; 279 280 foreach(module_; Modules) { 281 ret ~= getModuleWorksheetFunctions!module_; 282 } 283 284 return ret; 285 } 286 287 /** 288 Implements the getWorksheetFunctions function needed by xlld.xll in 289 order to register the Excel-callable functions at runtime 290 This used to be a template mixin but even using a string mixin inside 291 fails to actually make it an extern(C) function. 292 */ 293 string implGetWorksheetFunctionsString(Modules...)() if(allSatisfy!(isSomeString, typeof(Modules))) { 294 return implGetWorksheetFunctionsString(Modules); 295 } 296 297 @("template mixin for getWorkSheetFunctions for test_xl_funcs") 298 unittest { 299 import xlld.traits; 300 import xlld.worksheet; 301 302 // mixin the function here then call it to see if it does what it's supposed to 303 mixin(implGetWorksheetFunctionsString!"xlld.test_xl_funcs"); 304 getWorksheetFunctions.shouldEqual( 305 [ 306 doubleToDoubleFunction("FuncMulByTwo"), 307 FP12ToDoubleFunction("FuncFP12"), 308 operToOperFunction("FuncFib"), 309 ] 310 ); 311 } 312 313 string implGetWorksheetFunctionsString(string[] modules...) { 314 import std.array: join; 315 316 if(!__ctfe) { 317 return ""; 318 } 319 320 string modulesString() { 321 322 string[] ret; 323 foreach(module_; modules) { 324 ret ~= `"` ~ module_ ~ `"`; 325 } 326 return ret.join(", "); 327 } 328 329 return 330 [ 331 `extern(C) WorksheetFunction[] getWorksheetFunctions() @safe pure nothrow {`, 332 ` return getAllWorksheetFunctions!(` ~ modulesString ~ `);`, 333 `}`, 334 ].join("\n"); 335 } 336 337 @("implGetWorksheetFunctionsString runtime") 338 unittest { 339 import xlld.traits; 340 import xlld.worksheet; 341 342 // mixin the function here then call it to see if it does what it's supposed to 343 mixin(implGetWorksheetFunctionsString("xlld.test_xl_funcs")); 344 getWorksheetFunctions.shouldEqual( 345 [ 346 doubleToDoubleFunction("FuncMulByTwo"), 347 FP12ToDoubleFunction("FuncFP12"), 348 operToOperFunction("FuncFib"), 349 ] 350 ); 351 } 352 353 354 355 struct DllDefFile { 356 Statement[] statements; 357 } 358 359 struct Statement { 360 string name; 361 string[] args; 362 363 this(string name, string[] args) @safe pure nothrow { 364 this.name = name; 365 this.args = args; 366 } 367 368 this(string name, string arg) @safe pure nothrow { 369 this(name, [arg]); 370 } 371 372 string toString() @safe pure const { 373 import std.array: join; 374 import std.algorithm: map; 375 376 if(name == "EXPORTS") 377 return name ~ "\n" ~ args.map!(a => "\t\t" ~ a).join("\n"); 378 else 379 return name ~ "\t\t" ~ args.map!(a => stringify(name, a)).join(" "); 380 } 381 382 static private string stringify(in string name, in string arg) @safe pure { 383 if(name == "LIBRARY") return `"` ~ arg ~ `"`; 384 if(name == "DESCRIPTION") return `'` ~ arg ~ `'`; 385 return arg; 386 } 387 } 388 389 /** 390 Returns a structure descripting a Windows .def file. 391 This allows the tests to not care about the specific formatting 392 used when writing the information out. 393 This encapsulates all the functions to be exported by the DLL/XLL. 394 */ 395 DllDefFile dllDefFile(Modules...)(string libName, string description) 396 if(allSatisfy!(isSomeString, typeof(Modules))) 397 { 398 import std.conv: to; 399 400 auto statements = [ 401 Statement("LIBRARY", libName), 402 ]; 403 404 string[] exports = ["xlAutoOpen", "xlAutoClose", "xlAutoFree12"]; 405 foreach(func; getAllWorksheetFunctions!Modules) { 406 exports ~= func.procedure.to!string; 407 } 408 409 return DllDefFile(statements ~ Statement("EXPORTS", exports)); 410 } 411 412 @("worksheet functions to .def file") 413 unittest { 414 dllDefFile!"xlld.test_xl_funcs"("myxll32.dll", "Simple D add-in").shouldEqual( 415 DllDefFile( 416 [ 417 Statement("LIBRARY", "myxll32.dll"), 418 Statement("EXPORTS", ["xlAutoOpen", "xlAutoClose", "xlAutoFree12", "FuncMulByTwo", "FuncFP12", "FuncFib"]), 419 ] 420 ) 421 ); 422 } 423 424 425 mixin template GenerateDllDef(string module_ = __MODULE__) { 426 version(exceldDef) { 427 void main(string[] args) nothrow { 428 import xlld.traits: generateDllDef; 429 try { 430 generateDllDef!module_(args); 431 } catch(Exception ex) { 432 import std.stdio: stderr; 433 try 434 stderr.writeln("Error: ", ex.msg); 435 catch(Exception ex2) 436 assert(0, "Program could not write exception message"); 437 } 438 } 439 } 440 } 441 442 void generateDllDef(string module_ = __MODULE__)(string[] args) { 443 import std.stdio: File; 444 import std.exception: enforce; 445 import std.path: stripExtension; 446 447 enforce(args.length >= 2 && args.length <= 4, 448 "Usage: " ~ args[0] ~ " [file_name] <lib_name> <description>"); 449 450 immutable fileName = args[1]; 451 immutable libName = args.length > 2 452 ? args[2] 453 : fileName.stripExtension ~ ".xll"; 454 immutable description = args.length > 3 455 ? args[3] 456 : "Simple D add-in to Excel"; 457 458 auto file = File(fileName, "w"); 459 foreach(stmt; dllDefFile!module_(libName, description).statements) 460 file.writeln(stmt.toString); 461 }