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 }