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 @safe pure unittest {
148     static assert(isOneOf!(int, int, int));
149     static assert(!isOneOf!(int, double, string));
150 }
151 
152 // whether or not this is a function that can be called from Excel
153 template isWorksheetFunction(alias F) {
154     static if(isWorksheetFunctionModuloLinkage!F) {
155         import std.traits: functionLinkage;
156         enum isWorksheetFunction = functionLinkage!F == "Windows";
157     } else
158         enum isWorksheetFunction = false;
159 }
160 
161 /// if the types match for a worksheet function but without checking the linkage
162 template isWorksheetFunctionModuloLinkage(alias F) {
163     import std.traits: ReturnType, Parameters, isCallable;
164     import std.meta: anySatisfy;
165 
166     static if(!isCallable!F)
167         enum isWorksheetFunctionModuloLinkage = false;
168     else {
169 
170         enum isEnum(T) = is(T == enum);
171         enum isOneOfSupported(U) = isOneOf!(U, double, FP12*, LPXLOPER12);
172 
173         enum isWorksheetFunctionModuloLinkage =
174             isSupportedFunction!(F, isOneOfSupported) &&
175             !is(ReturnType!F == enum) &&
176             !anySatisfy!(isEnum, Parameters!F);
177     }
178 }
179 
180 
181 /**
182  Gets all Excel-callable functions in a given module
183  */
184 WorksheetFunction[] getModuleWorksheetFunctions(string moduleName)
185                                                (Flag!"onlyExports" onlyExports = No.onlyExports)
186 {
187     mixin(`import ` ~ moduleName ~ `;`);
188     alias module_ = Identity!(mixin(moduleName));
189 
190     WorksheetFunction[] ret;
191 
192     foreach(moduleMemberStr; __traits(allMembers, module_)) {
193 
194         alias moduleMember = Identity!(__traits(getMember, module_, moduleMemberStr));
195 
196         static if(isWorksheetFunction!moduleMember) {
197             try {
198                 const shouldWrap = onlyExports ? __traits(getProtection, moduleMember) == "export" : true;
199                 if(shouldWrap)
200                     ret ~= getWorksheetFunction!(moduleMember);
201             } catch(Exception ex)
202                 assert(0); //can't happen
203         } else static if(isWorksheetFunctionModuloLinkage!moduleMember) {
204             import std.traits: functionLinkage;
205             pragma(msg, "!!!!! excel-d warning: function " ~ __traits(identifier, moduleMember) ~
206                    " has the right types to be callable from Excel but isn't due to having " ~
207                    functionLinkage!moduleMember ~ " linkage instead of the required 'Windows'");
208         }
209     }
210 
211     return ret;
212 }
213 
214 /**
215  Gets all Excel-callable functions from the given modules
216  */
217 WorksheetFunction[] getAllWorksheetFunctions(Modules...)
218                                             (Flag!"onlyExports" onlyExports = No.onlyExports)
219     pure @safe if(allSatisfy!(isSomeString, typeof(Modules)))
220 {
221     WorksheetFunction[] ret;
222 
223     foreach(module_; Modules) {
224         ret ~= getModuleWorksheetFunctions!module_(onlyExports);
225     }
226 
227     return ret;
228 }
229 
230 /**
231  Implements the getWorksheetFunctions function needed by xlld.sdk.xll in
232  order to register the Excel-callable functions at runtime
233  This used to be a template mixin but even using a string mixin inside
234  fails to actually make it an extern(C) function.
235  */
236 string implGetWorksheetFunctionsString(Modules...)() if(allSatisfy!(isSomeString, typeof(Modules))) {
237     return implGetWorksheetFunctionsString(Modules);
238 }
239 
240 
241 string implGetWorksheetFunctionsString(string[] modules...) {
242     import std.array: join;
243 
244     if(!__ctfe) {
245         return "";
246     }
247 
248     string modulesString() {
249 
250         string[] ret;
251         foreach(module_; modules) {
252             ret ~= `"` ~ module_ ~ `"`;
253         }
254         return ret.join(", ");
255     }
256 
257     return
258         [
259             `extern(C) WorksheetFunction[] getWorksheetFunctions() @safe pure nothrow {`,
260             `    return getAllWorksheetFunctions!(` ~ modulesString ~ `);`,
261             `}`,
262         ].join("\n");
263 }
264 
265 
266 ///
267 struct DllDefFile {
268     Statement[] statements;
269 }
270 
271 ///
272 struct Statement {
273     string name;
274     string[] args;
275 
276     this(string name, string[] args) @safe pure nothrow {
277         this.name = name;
278         this.args = args;
279     }
280 
281     this(string name, string arg) @safe pure nothrow {
282         this(name, [arg]);
283     }
284 
285     string toString() @safe pure const {
286         import std.array: join;
287         import std.algorithm: map;
288 
289         if(name == "EXPORTS")
290             return name ~ "\n" ~ args.map!(a => "\t\t" ~ a).join("\n");
291         else
292             return name ~ "\t\t" ~ args.map!(a => stringify(name, a)).join(" ");
293     }
294 
295     static private string stringify(in string name, in string arg) @safe pure {
296         if(name == "LIBRARY") return `"` ~ arg ~ `"`;
297         if(name == "DESCRIPTION") return `'` ~ arg ~ `'`;
298         return arg;
299     }
300 }
301 
302 /**
303    Returns a structure descripting a Windows .def file.
304    This allows the tests to not care about the specific formatting
305    used when writing the information out.
306    This encapsulates all the functions to be exported by the DLL/XLL.
307  */
308 DllDefFile dllDefFile(Modules...)
309                      (string libName,
310                       string description,
311                       Flag!"onlyExports" onlyExports = No.onlyExports)
312     if(allSatisfy!(isSomeString, typeof(Modules)))
313 {
314     import std.conv: to;
315 
316     auto statements = [
317         Statement("LIBRARY", libName),
318     ];
319 
320     string[] exports = ["xlAutoOpen", "xlAutoClose", "xlAutoFree12"];
321     foreach(func; getAllWorksheetFunctions!Modules(onlyExports)) {
322         exports ~= func.procedure.to!string;
323     }
324 
325     return DllDefFile(statements ~ Statement("EXPORTS", exports));
326 }
327 
328 
329 ///
330 mixin template GenerateDllDef(string module_ = __MODULE__) {
331     version(exceldDef) {
332         void main(string[] args) nothrow {
333             import xlld.wrap.traits: generateDllDef;
334             try {
335                 generateDllDef!module_(args);
336             } catch(Exception ex) {
337                 import std.stdio: stderr;
338                 try
339                     stderr.writeln("Error: ", ex.msg);
340                 catch(Exception ex2)
341                     assert(0, "Program could not write exception message");
342             }
343         }
344     }
345 }
346 
347 ///
348 void generateDllDef(string module_ = __MODULE__,
349                     Flag!"onlyExports" onlyExports = No.onlyExports)
350                    (string[] args)
351 {
352     import std.stdio: File;
353     import std.exception: enforce;
354     import std.path: stripExtension;
355 
356     enforce(args.length >= 2 && args.length <= 4,
357             "Usage: " ~ args[0] ~ " [file_name] <lib_name> <description>");
358 
359     immutable fileName = args[1];
360     immutable libName = args.length > 2
361         ? args[2]
362         : fileName.stripExtension ~ ".xll";
363     immutable description = args.length > 3
364         ? args[3]
365         : "Simple D add-in to Excel";
366 
367     auto file = File(fileName, "w");
368     foreach(stmt; dllDefFile!module_(libName, description, onlyExports).statements)
369         file.writeln(stmt.toString);
370 }
371 
372 /**
373    UDA for functions to be executed asynchronously
374  */
375 enum Async;
376 
377 version(unittest) {
378 // to link
379     extern(C) auto getWorksheetFunctions() @safe pure nothrow {
380         import xlld: WorksheetFunction;
381         WorksheetFunction[] ret;
382         return ret;
383     }
384 }