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.wrap.traits;
18 
19 import xlld.wrap.worksheet;
20 import xlld.sdk.xlcall;
21 import std.traits: isSomeFunction, isSomeString;
22 import std.meta: allSatisfy;
23 import std.typecons: Flag, No;
24 
25 /// import unit_threaded and introduce helper functions for testing
26 version(testingExcelD) {
27     import unit_threaded;
28 }
29 
30 /**
31  Take a D function as a compile-time parameter and returns a
32  WorksheetFunction struct with the fields filled in accordingly.
33  */
34 WorksheetFunction getWorksheetFunction(alias F)() if(isSomeFunction!F) {
35     import xlld.wrap.wrap: pascalCase = toPascalCase;
36     import std.traits: ReturnType, Parameters, getUDAs, ParameterIdentifierTuple;
37     import std.conv: text, to;
38     import std.meta: Filter;
39     import std.algorithm: among, map;
40     import std.array: join;
41 
42     alias R = ReturnType!F;
43     alias T = Parameters!F;
44 
45     static if(!isWorksheetFunction!F) {
46         throw new Exception("Unsupported function type " ~ R.stringof ~ T.stringof ~ " for " ~
47                             __traits(identifier, F).stringof[1 .. $-1]);
48     } else {
49 
50         WorksheetFunction ret;
51         auto name = __traits(identifier, F).pascalCase.to!wstring;
52         ret.procedure = Procedure(name);
53         ret.functionText = FunctionText(name);
54         ret.typeText = TypeText(getTypeText!F);
55 
56         // check to see if decorated with @Register
57         alias registerAttrs = getUDAs!(F, Register);
58         static if(registerAttrs.length > 0) {
59             static assert(registerAttrs.length == 1,
60                           text("Only 1 @Register allowed, found ", registerAttrs.length,
61                                " on function ", __traits(identifier, F)));
62             ret.optional = registerAttrs[0];
63         }
64 
65         enum isExcelParam(alias T) = is(typeof(T) == ExcelParameter);
66 
67         static foreach(i; 0 .. Parameters!F.length) {{
68             alias udas = getParamUDAs!(F, i);
69             static if(udas.length) {
70                 enum excelParamUDAs = Filter!(isExcelParam, udas);
71                 static assert(excelParamUDAs.length.among(0, 1), "Maxinum one @ExcelParameter allowed");
72                 static if(excelParamUDAs.length)
73                     ret.optional.argumentHelp.add(excelParamUDAs[0].value);
74                 else
75                     ret.optional.argumentHelp.add("");
76             } else
77                 ret.optional.argumentHelp.add("");
78         }}
79 
80         static if(Parameters!F.length) {
81             if(ret.optional.argumentText.value == ""w) {
82                 ret.optional.argumentText = [ParameterIdentifierTuple!F]
83                     .map!(a => a.to!wstring)
84                     .join(";"w)
85                     .ArgumentText
86                     ;
87             }
88         }
89 
90         return ret;
91     }
92 }
93 
94 template getParamUDAs(alias fun, size_t paramIdx)
95 {
96     alias P = BetterParams!fun[paramIdx .. paramIdx+1];
97     static if(__traits(compiles, __traits(getAttributes, P)))
98         alias getParamUDAs = __traits(getAttributes, P);
99     else {
100         import std.meta: AliasSeq;
101         alias getParamUDAs = AliasSeq!();
102     }
103 }
104 
105 
106 import std.traits: isCallable;
107 template BetterParams(func...)
108 if (func.length == 1 && isCallable!func)
109 {
110     import std.traits : FunctionTypeOf;
111     static if (is(FunctionTypeOf!func P == __parameters))
112         alias BetterParams = P;
113     else
114         static assert(0, "argument has no parameters");
115 }
116 
117 
118 
119 wstring getTypeText(alias F)() if(isSomeFunction!F) {
120     import std.traits: ReturnType, Parameters, Unqual, hasUDA;
121 
122     wstring typeToString(T)() {
123 
124         alias Type = Unqual!T;
125 
126         static if(is(Type == double))
127             return "B";
128         else static if(is(Type == FP12*))
129             return "K%";
130         else static if(is(Type == LPXLOPER12))
131             return "U";
132         else static if(is(Type == void))
133             return ">";
134         else
135             static assert(false, "Unsupported type " ~ T.stringof);
136     }
137 
138     auto retType = typeToString!(ReturnType!F);
139     foreach(i, argType; Parameters!F) {
140         static if(i == Parameters!F.length - 1 && hasUDA!(F, Async))
141             retType ~= "X";
142         else
143             retType ~= typeToString!(argType);
144 
145     }
146 
147     return retType;
148 }
149 
150 
151 
152 // helper template for aliasing
153 alias Identity(alias T) = T;
154 
155 
156 /**
157    Is true if F is a callable function and functionTypePredicate is true
158    for the return type and all parameter types of F.
159  */
160 template isSupportedFunction(alias F, alias functionTypePredicate) {
161     import std.traits: ReturnType, Parameters;
162     import std.meta: allSatisfy;
163 
164     static if(isCallableFunction!F) {
165         enum returnTypeOk = functionTypePredicate!(ReturnType!F) || is(ReturnType!F == void);
166         enum paramTypesOk = allSatisfy!(functionTypePredicate, Parameters!F);
167         enum isSupportedFunction = returnTypeOk && paramTypesOk;
168     } else
169         enum isSupportedFunction = false;
170 }
171 
172 
173 template isCallableFunction(alias F) {
174     import std.traits: isSomeFunction, Parameters;
175     import std.typecons: Tuple;
176 
177     /// trying to get a pointer to something is a good way of making sure we can
178     /// attempt to evaluate `isSomeFunction` - it's not always possible
179     enum canGetPointerToIt = __traits(compiles, &F);
180 
181     static if(canGetPointerToIt) {
182         static if(isSomeFunction!F)
183             enum isCallableFunction = __traits(compiles, F(Tuple!(Parameters!F)().expand));
184          else
185              enum isCallableFunction = false;
186     } else
187         enum isCallableFunction = false;
188 }
189 
190 
191 // if T is one of A
192 template isOneOf(T, A...) {
193     static if(A.length == 0)
194         enum isOneOf = false;
195     else
196         enum isOneOf = is(T == A[0]) || isOneOf!(T, A[1..$]);
197 }
198 
199 @("isOneOf")
200 @safe pure unittest {
201     static assert(isOneOf!(int, int, int));
202     static assert(!isOneOf!(int, double, string));
203 }
204 
205 // whether or not this is a function that can be called from Excel
206 template isWorksheetFunction(alias F) {
207     static if(isWorksheetFunctionModuloLinkage!F) {
208         import std.traits: functionLinkage;
209         enum isWorksheetFunction = functionLinkage!F == "Windows";
210     } else
211         enum isWorksheetFunction = false;
212 }
213 
214 /// if the types match for a worksheet function but without checking the linkage
215 template isWorksheetFunctionModuloLinkage(alias F) {
216     import std.traits: ReturnType, Parameters, isCallable;
217     import std.meta: anySatisfy;
218 
219     static if(!isCallable!F)
220         enum isWorksheetFunctionModuloLinkage = false;
221     else {
222 
223         enum isEnum(T) = is(T == enum);
224         enum isOneOfSupported(U) = isOneOf!(U, double, FP12*, LPXLOPER12);
225 
226         enum isWorksheetFunctionModuloLinkage =
227             isSupportedFunction!(F, isOneOfSupported) &&
228             !is(ReturnType!F == enum) &&
229             !anySatisfy!(isEnum, Parameters!F);
230     }
231 }
232 
233 
234 /**
235  Gets all Excel-callable functions in a given module
236  */
237 WorksheetFunction[] getModuleWorksheetFunctions(string moduleName)
238                                                (Flag!"onlyExports" onlyExports = No.onlyExports)
239 {
240     mixin(`import ` ~ moduleName ~ `;`);
241     alias module_ = Identity!(mixin(moduleName));
242 
243     WorksheetFunction[] ret;
244 
245     foreach(moduleMemberStr; __traits(allMembers, module_)) {
246 
247         static if(__traits(compiles, Identity!(__traits(getMember, module_, moduleMemberStr)))) {
248             alias moduleMember = Identity!(__traits(getMember, module_, moduleMemberStr));
249 
250             static if(isWorksheetFunction!moduleMember) {
251                 try {
252                     const shouldWrap = onlyExports ? __traits(getProtection, moduleMember) == "export" : true;
253                     if(shouldWrap)
254                         ret ~= getWorksheetFunction!(moduleMember);
255                 } catch(Exception ex)
256                     assert(0); //can't happen
257             } else static if(isWorksheetFunctionModuloLinkage!moduleMember) {
258                 import std.traits: functionLinkage;
259                 pragma(msg, "!!!!! excel-d warning: function " ~ __traits(identifier, moduleMember) ~
260                        " has the right types to be callable from Excel but isn't due to having " ~
261                        functionLinkage!moduleMember ~ " linkage instead of the required 'Windows'");
262             }
263         }
264     }
265 
266     return ret;
267 }
268 
269 /**
270  Gets all Excel-callable functions from the given modules
271  */
272 WorksheetFunction[] getAllWorksheetFunctions(Modules...)
273                                             (Flag!"onlyExports" onlyExports = No.onlyExports)
274     pure @safe if(allSatisfy!(isSomeString, typeof(Modules)))
275 {
276     WorksheetFunction[] ret;
277 
278     foreach(module_; Modules) {
279         ret ~= getModuleWorksheetFunctions!module_(onlyExports);
280     }
281 
282     return ret;
283 }
284 
285 /**
286  Implements the getWorksheetFunctions function needed by xlld.sdk.xll in
287  order to register the Excel-callable functions at runtime
288  This used to be a template mixin but even using a string mixin inside
289  fails to actually make it an extern(C) function.
290  */
291 string implGetWorksheetFunctionsString(Modules...)() if(allSatisfy!(isSomeString, typeof(Modules))) {
292     return implGetWorksheetFunctionsString(Modules);
293 }
294 
295 
296 string implGetWorksheetFunctionsString(string[] modules...) {
297     import std.array: join;
298 
299     if(!__ctfe) {
300         return "";
301     }
302 
303     string modulesString() {
304 
305         string[] ret;
306         foreach(module_; modules) {
307             ret ~= `"` ~ module_ ~ `"`;
308         }
309         return ret.join(", ");
310     }
311 
312     return
313         [
314             `extern(C) WorksheetFunction[] getWorksheetFunctions() @safe pure nothrow {`,
315             `    return getAllWorksheetFunctions!(` ~ modulesString ~ `);`,
316             `}`,
317         ].join("\n");
318 }
319 
320 
321 ///
322 struct DllDefFile {
323     Statement[] statements;
324 }
325 
326 ///
327 struct Statement {
328     string name;
329     string[] args;
330 
331     this(string name, string[] args) @safe pure nothrow {
332         this.name = name;
333         this.args = args;
334     }
335 
336     this(string name, string arg) @safe pure nothrow {
337         this(name, [arg]);
338     }
339 
340     string toString() @safe pure const {
341         import std.array: join;
342         import std.algorithm: map;
343 
344         if(name == "EXPORTS")
345             return name ~ "\n" ~ args.map!(a => "\t\t" ~ a).join("\n");
346         else
347             return name ~ "\t\t" ~ args.map!(a => stringify(name, a)).join(" ");
348     }
349 
350     static private string stringify(in string name, in string arg) @safe pure {
351         if(name == "LIBRARY") return `"` ~ arg ~ `"`;
352         if(name == "DESCRIPTION") return `'` ~ arg ~ `'`;
353         return arg;
354     }
355 }
356 
357 /**
358    Returns a structure descripting a Windows .def file.
359    This allows the tests to not care about the specific formatting
360    used when writing the information out.
361    This encapsulates all the functions to be exported by the DLL/XLL.
362  */
363 DllDefFile dllDefFile(Modules...)
364                      (string libName,
365                       string description,
366                       Flag!"onlyExports" onlyExports = No.onlyExports)
367     if(allSatisfy!(isSomeString, typeof(Modules)))
368 {
369     import std.conv: to;
370 
371     auto statements = [
372         Statement("LIBRARY", libName),
373     ];
374 
375     string[] exports = ["xlAutoOpen", "xlAutoClose", "xlAutoFree12"];
376     foreach(func; getAllWorksheetFunctions!Modules(onlyExports)) {
377         exports ~= func.procedure.to!string;
378     }
379 
380     return DllDefFile(statements ~ Statement("EXPORTS", exports));
381 }
382 
383 
384 ///
385 mixin template GenerateDllDef(string module_ = __MODULE__) {
386     version(exceldDef) {
387         void main(string[] args) nothrow {
388             import xlld.wrap.traits: generateDllDef;
389             try {
390                 generateDllDef!module_(args);
391             } catch(Exception ex) {
392                 import std.stdio: stderr;
393                 try
394                     stderr.writeln("Error: ", ex.msg);
395                 catch(Exception ex2)
396                     assert(0, "Program could not write exception message");
397             }
398         }
399     }
400 }
401 
402 ///
403 void generateDllDef(string module_ = __MODULE__,
404                     Flag!"onlyExports" onlyExports = No.onlyExports)
405                    (string[] args)
406 {
407     import std.stdio: File;
408     import std.exception: enforce;
409     import std.path: stripExtension;
410 
411     enforce(args.length >= 2 && args.length <= 4,
412             "Usage: " ~ args[0] ~ " [file_name] <lib_name> <description>");
413 
414     immutable fileName = args[1];
415     immutable libName = args.length > 2
416         ? args[2]
417         : fileName.stripExtension ~ ".xll";
418     immutable description = args.length > 3
419         ? args[3]
420         : "Simple D add-in to Excel";
421 
422     auto file = File(fileName, "w");
423     foreach(stmt; dllDefFile!module_(libName, description, onlyExports).statements)
424         file.writeln(stmt.toString);
425 }
426 
427 /**
428    UDA for functions to be executed asynchronously
429  */
430 enum Async;