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