1 /**
2     Wrapper module for Excel. This module contains the functionality that autowraps
3     D code for use within Excel.
4 */
5 module xlld.wrap.wrap;
6 
7 import xlld.wrap.worksheet;
8 import xlld.sdk.xlcall: XLOPER12;
9 import std.typecons: Flag, No;
10 
11 
12 
13 
14 ///
15 string wrapAll(Modules...)
16               (Flag!"onlyExports" onlyExports = No.onlyExports,
17                in string mainModule = __MODULE__)
18 {
19 
20     if(!__ctfe) {
21         return "";
22     }
23 
24     import xlld.wrap.traits: implGetWorksheetFunctionsString;
25     return
26         wrapWorksheetFunctionsString!Modules(onlyExports, mainModule) ~
27         "\n" ~
28         implGetWorksheetFunctionsString!(mainModule) ~
29         "\n" ~
30         `mixin GenerateDllDef!"` ~ mainModule ~ `";` ~
31         "\n";
32 }
33 
34 ///
35 string wrapWorksheetFunctionsString(Modules...)
36                                    (Flag!"onlyExports" onlyExports = No.onlyExports, string callingModule = __MODULE__)
37 {
38     if(!__ctfe) {
39         return "";
40     }
41 
42     string ret;
43     foreach(module_; Modules) {
44         ret ~= wrapModuleWorksheetFunctionsString!module_(onlyExports, callingModule);
45     }
46 
47     return ret;
48 }
49 
50 
51 /**
52    A string to mixin that wraps all eligible functions in the
53    given module.
54  */
55 string wrapModuleWorksheetFunctionsString(string moduleName)
56                                          (Flag!"onlyExports" onlyExports = No.onlyExports, string callingModule = __MODULE__)
57 {
58     if(!__ctfe) {
59         return "";
60     }
61 
62     import xlld.wrap.traits: Identity;
63 
64     mixin(`import ` ~ moduleName ~ `;`);
65     alias module_ = Identity!(mixin(moduleName));
66 
67     string ret;
68 
69     foreach(moduleMemberStr; __traits(allMembers, module_))
70         ret ~= wrapModuleMember!(moduleName, moduleMemberStr)(onlyExports, callingModule);
71 
72     return ret;
73 }
74 
75 string wrapModuleMember(string moduleName, string moduleMemberStr)
76                        (Flag!"onlyExports" onlyExports = No.onlyExports, string callingModule = __MODULE__)
77 {
78     if(!__ctfe) return "";
79 
80     import xlld.wrap.traits: Identity;
81     import std.traits: functionAttributes, FunctionAttribute;
82 
83     mixin(`import ` ~ moduleName ~ `;`);
84     alias module_ = Identity!(mixin(moduleName));
85 
86     string ret;
87 
88     alias moduleMember = Identity!(__traits(getMember, module_, moduleMemberStr));
89 
90     static if(isWorksheetFunction!moduleMember) {
91         enum numOverloads = __traits(getOverloads, mixin(moduleName), moduleMemberStr).length;
92         static if(numOverloads == 1) {
93             // if onlyExports is true, then only functions that are "export" are allowed
94             // Otherwise, any function will do as long as they're visible (i.e. public)
95             const shouldWrap = onlyExports ? __traits(getProtection, moduleMember) == "export" : true;
96             if(shouldWrap)
97                 ret ~= wrapModuleFunctionStr!(moduleName, moduleMemberStr)(callingModule);
98         } else
99             pragma(msg, "excel-d WARNING: Not wrapping ", moduleMemberStr, " due to it having ",
100                    cast(int)numOverloads, " overloads");
101     } else {
102         /// trying to get a pointer to something is a good way of making sure we can
103         /// attempt to evaluate `isSomeFunction` - it's not always possible
104         enum canGetPointerToIt = __traits(compiles, &moduleMember);
105         static if(canGetPointerToIt) {
106             import xlld.wrap.worksheet: Register;
107             import std.traits: getUDAs;
108             alias registerAttrs = getUDAs!(moduleMember, Register);
109             static assert(registerAttrs.length == 0,
110                           "excel-d ERROR: Function `" ~ moduleMemberStr ~ "` not eligible for wrapping");
111         }
112     }
113 
114     return ret;
115 }
116 
117 /**
118  A string to use with `mixin` that wraps a D function
119  */
120 string wrapModuleFunctionStr(string moduleName, string funcName)
121                             (in string callingModule = __MODULE__)
122 {
123     if(!__ctfe) {
124         return "";
125     }
126 
127     assert(callingModule != moduleName,
128            "Cannot use `wrapAll` with __MODULE__");
129 
130     import xlld.wrap.traits: Async, Identity;
131     import xlld.wrap.worksheet: Register;
132     import std.array: join;
133     import std.traits: Parameters, functionAttributes, FunctionAttribute, getUDAs, hasUDA;
134     import std.conv: to;
135     import std.algorithm: map;
136     import std.range: iota;
137     import std.format: format;
138 
139     mixin("import " ~ moduleName ~ ": " ~ funcName ~ ";");
140 
141     alias func = Identity!(mixin(funcName));
142 
143     const argsLength = Parameters!(mixin(funcName)).length;
144     // e.g. XLOPER12* arg0, XLOPER12* arg1, ...
145     auto argsDecl = argsLength.iota.map!(a => `XLOPER12* arg` ~ a.to!string).join(", ");
146     // e.g. arg0, arg1, ...
147     static if(!hasUDA!(func, Async))
148         const argsCall = argsLength.iota.map!(a => `arg` ~ a.to!string).join(", ");
149     else {
150         import std.range: only, chain, iota;
151         import std.conv: text;
152         argsDecl ~= ", XLOPER12* asyncHandle";
153         const argsCall = chain(only("*asyncHandle"), argsLength.iota.map!(a => `*arg` ~ a.text)).
154             map!(a => `cast(immutable)` ~ a)
155             .join(", ");
156     }
157     const nogc = functionAttributes!(mixin(funcName)) & FunctionAttribute.nogc
158         ? "@nogc "
159         : "";
160     const safe = functionAttributes!(mixin(funcName)) & FunctionAttribute.safe
161         ? "@trusted "
162         : "";
163 
164     alias registerAttrs = getUDAs!(mixin(funcName), Register);
165     static assert(registerAttrs.length == 0 || registerAttrs.length == 1,
166                   "Invalid number of @Register on " ~ funcName);
167 
168     string register;
169     static if(registerAttrs.length)
170         register = `@` ~ registerAttrs[0].to!string;
171     string async;
172     static if(hasUDA!(func, Async))
173         async = "@Async";
174 
175     const returnType = hasUDA!(func, Async) ? "void" : "XLOPER12*";
176     // The function name that Excel actually calls in the binary
177     const xlFuncName = pascalCase(funcName);
178     const return_ = hasUDA!(func, Async) ? "" : "return ";
179     const wrap = hasUDA!(func, Async)
180         ? q{wrapAsync!wrappedFunc(Mallocator.instance, %s)}.format(argsCall)
181         : q{wrapModuleFunctionImpl!wrappedFunc(gTempAllocator, %s)}.format(argsCall);
182 
183     return [
184         register,
185         async,
186         q{
187             extern(Windows) %s %s(%s) nothrow %s %s {
188                 static import %s;
189                 import xlld.memorymanager: gTempAllocator;
190                 import std.experimental.allocator.mallocator: Mallocator;
191                 alias wrappedFunc = %s.%s;
192                 %s%s;
193             }
194         }.format(returnType, xlFuncName, argsDecl, nogc, safe,
195                  moduleName,
196                  moduleName, funcName,
197                  return_, wrap),
198     ].join("\n");
199 }
200 
201 string pascalCase(in string func) @safe pure {
202     import std.uni: toUpper;
203     import std.conv: to;
204     return (func[0].toUpper ~ func[1..$].to!dstring).to!string;
205 }
206 
207 void wrapAsync(alias F, A, T...)(ref A allocator, immutable XLOPER12 asyncHandle, T args) {
208 
209     import xlld.sdk.xlcall: XlType, xlAsyncReturn;
210     import xlld.sdk.framework: Excel12f;
211     import std.concurrency: spawn;
212     import std.format: format;
213 
214     string toDArgsStr() {
215         import std.string: join;
216         import std.conv: text;
217 
218         string[] pointers;
219         foreach(i; 0 .. T.length) {
220             pointers ~= text("&args[", i, "]");
221         }
222 
223         return q{toDArgs!F(allocator, %s)}.format(pointers.join(", "));
224     }
225 
226     mixin(q{alias DArgs = typeof(%s);}.format(toDArgsStr));
227     DArgs dArgs;
228 
229     // Convert all Excel types to D types. This needs to be done here because the
230     // asynchronous part of the computation can't call back into Excel, and converting
231     // to D types requires calling coerce.
232     try {
233         mixin(q{dArgs = %s;}.format(toDArgsStr));
234     } catch(Throwable t) {
235         static if(isGC!F) {
236             import xlld.sdk.xll: log;
237             log("ERROR: Could not convert to D args for asynchronous function " ~
238                 __traits(identifier, F));
239         }
240     }
241 
242     try
243         spawn(&wrapAsyncImpl!(F, A, DArgs), allocator, asyncHandle, cast(immutable)dArgs);
244     catch(Exception ex) {
245         XLOPER12 functionRet, ret;
246         functionRet.xltype = XlType.xltypeErr;
247         Excel12f(xlAsyncReturn, &ret, cast(XLOPER12*)&asyncHandle, &functionRet);
248     }
249 }
250 
251 
252 void wrapAsyncImpl(alias F, A, T)(ref A allocator, XLOPER12 asyncHandle, T dArgs) {
253     import xlld.sdk.framework: Excel12f;
254     import xlld.sdk.xlcall: xlAsyncReturn;
255     import std.traits: Unqual;
256 
257     // get rid of the temporary memory allocations for the conversions
258     scope(exit) freeDArgs(allocator, cast(Unqual!T)dArgs);
259 
260     auto functionRet = callWrapped!F(dArgs);
261     XLOPER12 xl12ret;
262     const errorCode = () @trusted {
263         return Excel12f(xlAsyncReturn, &xl12ret, &asyncHandle, functionRet);
264     }();
265 }
266 
267 // if a function is not @nogc, i.e. it uses the GC
268 private bool isGC(alias F)() {
269     import std.traits: functionAttributes, FunctionAttribute;
270     enum nogc = functionAttributes!F & FunctionAttribute.nogc;
271     return !nogc;
272 }
273 
274 
275 /**
276  Implement a wrapper for a regular D function
277  */
278 XLOPER12* wrapModuleFunctionImpl(alias wrappedFunc, A, T...)
279                                   (ref A allocator, T args) {
280     static XLOPER12 ret;
281 
282     alias DArgs = typeof(toDArgs!wrappedFunc(allocator, args));
283     DArgs dArgs;
284     // convert all Excel types to D types
285     try {
286         dArgs = toDArgs!wrappedFunc(allocator, args);
287     } catch(Exception ex) {
288         ret = stringOper("#ERROR converting argument to call " ~ __traits(identifier, wrappedFunc));
289         return &ret;
290     } catch(Throwable t) {
291         ret = stringOper("#FATAL ERROR converting argument to call " ~ __traits(identifier, wrappedFunc));
292         return &ret;
293     }
294 
295     // get rid of the temporary memory allocations for the conversions
296     scope(exit) freeDArgs(allocator, dArgs);
297 
298     return callWrapped!wrappedFunc(dArgs);
299 }
300 
301 /**
302    Converts a variadic number of XLOPER12* to their equivalent D types
303    and returns a tuple
304  */
305 auto toDArgs(alias wrappedFunc, A, T...)
306             (ref A allocator, T args)
307 {
308     import xlld.func.xl: coerce, free;
309     import xlld.sdk.xlcall: XlType;
310     import xlld.conv.from: fromXlOper;
311     import std.traits: Parameters, Unqual;
312     import std.typecons: Tuple;
313     import std.meta: staticMap;
314 
315     static XLOPER12 ret;
316 
317     XLOPER12[T.length] realArgs;
318     // must 1st convert each argument to the "real" type.
319     // 2D arrays are passed in as SRefs, for instance
320     foreach(i, InputType; Parameters!wrappedFunc) {
321         if(args[i].xltype == XlType.xltypeMissing) {
322             realArgs[i] = *args[i];
323             continue;
324         }
325         realArgs[i] = coerce(args[i]);
326     }
327 
328     // scopedCoerce doesn't work with actual Excel
329     scope(exit) {
330         foreach(ref arg; realArgs)
331             free(&arg);
332     }
333 
334     // the D types to pass to the wrapped function
335     Tuple!(staticMap!(Unqual, Parameters!wrappedFunc)) dArgs;
336 
337     // convert all Excel types to D types
338     foreach(i, InputType; Parameters!wrappedFunc) {
339         dArgs[i] = () @trusted { return fromXlOper!InputType(&realArgs[i], allocator); }();
340     }
341 
342     return dArgs;
343 }
344 
345 
346 // Takes a tuple returned by `toDArgs`, calls the wrapped function and returns
347 // the XLOPER12 result
348 private XLOPER12* callWrapped(alias wrappedFunc, T)(T dArgs) {
349 
350     import xlld.wrap.worksheet: Dispose;
351     import xlld.sdk.xlcall: XlType;
352     import nogc.conv: text;
353     import std.traits: hasUDA, getUDAs, ReturnType;
354 
355     static XLOPER12 ret;
356 
357      try {
358         // call the wrapped function with D types
359          static if(is(ReturnType!wrappedFunc == void)) {
360              wrappedFunc(dArgs.expand);
361              ret.xltype = XlType.xltypeNil;
362              return &ret;
363          } else {
364              auto wrappedRet = wrappedFunc(dArgs.expand);
365              ret = excelRet(wrappedRet);
366 
367              // dispose of the memory allocated in the wrapped function
368              static if(hasUDA!(wrappedFunc, Dispose)) {
369                  alias disposes = getUDAs!(wrappedFunc, Dispose);
370                  static assert(disposes.length == 1, "Too many @Dispose for " ~ wrappedFunc.stringof);
371                  disposes[0].dispose(wrappedRet);
372              }
373 
374              return &ret;
375          }
376 
377     } catch(Exception ex) {
378          ret = stringOper(text("#ERROR calling ", __traits(identifier, wrappedFunc), ": ", ex.msg));
379          return &ret;
380     } catch(Throwable t) {
381          ret = stringOper(text("#FATAL ERROR calling ", __traits(identifier, wrappedFunc), ": ", t.msg));
382          return &ret;
383     }
384 }
385 
386 
387 private XLOPER12 stringOper(in string msg) @safe @nogc nothrow {
388     import xlld.conv: toAutoFreeOper;
389     import xlld.sdk.xlcall: XlType;
390 
391     try
392         return () @trusted { return msg.toAutoFreeOper; }();
393     catch(Exception _) {
394         XLOPER12 ret;
395         ret.xltype = XlType.xltypeErr;
396         return ret;
397     }
398 }
399 
400 
401 
402 // get excel return value from D return value of wrapped function
403 XLOPER12 excelRet(T)(T wrappedRet) {
404 
405     import xlld.conv: toAutoFreeOper;
406     import xlld.conv.misc: stripMemoryBitmask;
407     import xlld.sdk.xlcall: XlType;
408     import std.traits: isArray;
409 
410     static if(isArray!(typeof(wrappedRet))) {
411 
412         // Excel crashes if it's returned an empty array, so stop that from happening
413         if(wrappedRet.length == 0) {
414             return "#ERROR: empty result".toAutoFreeOper;
415         }
416 
417         static if(isArray!(typeof(wrappedRet[0]))) {
418             if(wrappedRet[0].length == 0) {
419                 return "#ERROR: empty result".toAutoFreeOper;
420             }
421         }
422     }
423 
424     // convert the return value to an Excel type, tell Excel to call
425     // us back to free it afterwards
426     auto ret = toAutoFreeOper(wrappedRet);
427 
428     // convert 1D arrays called from a column into a column instead of the default row
429     static if(isArray!(typeof(wrappedRet))) {
430         static if(!isArray!(typeof(wrappedRet[0]))) { // 1D array
431             import xlld.func.xlf: xlfCaller = caller;
432             import std.algorithm: swap;
433 
434             try {
435                 auto caller = xlfCaller;
436                 if(caller.xltype.stripMemoryBitmask == XlType.xltypeSRef) {
437                     const isColumnCaller = caller.val.sref.ref_.colLast == caller.val.sref.ref_.colFirst;
438                     if(isColumnCaller) swap(ret.val.array.rows, ret.val.array.columns);
439                 }
440             } catch(Exception _) {}
441         }
442     }
443 
444     return ret;
445 }
446 
447 
448 
449 
450 private void freeDArgs(A, T)(ref A allocator, ref T dArgs) {
451     static if(__traits(compiles, allocator.deallocateAll))
452         allocator.deallocateAll;
453     else {
454         foreach(ref dArg; dArgs) {
455             import std.traits: isPointer, isArray;
456             static if(isArray!(typeof(dArg)))
457             {
458                 import std.experimental.allocator: disposeMultidimensionalArray;
459                 allocator.disposeMultidimensionalArray(dArg[]);
460             }
461             else
462                 static if(isPointer!(typeof(dArg)))
463                 {
464                     import std.experimental.allocator: dispose;
465                     allocator.dispose(dArg);
466                 }
467         }
468     }
469 }
470 
471 // if a function can be wrapped to be baclled by Excel
472 private template isWorksheetFunction(alias F) {
473     import xlld.wrap.traits: isSupportedFunction;
474     enum isWorksheetFunction = isSupportedFunction!(F, isWantedType);
475 }
476 
477 template isWantedType(T) {
478     import xlld.wrap.traits: isOneOf;
479     import xlld.any: Any;
480     import std.datetime: DateTime;
481     import std.traits: Unqual;
482 
483     alias U = Unqual!T;
484 
485     enum isOneOfTypes = isOneOf!(
486         U,
487         bool,
488         int,
489         double, double[], double[][],
490         string, string[], string[][],
491         Any, Any[], Any[][],
492         DateTime, DateTime[], DateTime[][],
493         );
494 
495     static if(isOneOfTypes)
496         enum isWantedType = true;
497     else static if(is(U == enum) || is(U == struct))
498         enum isWantedType = true;
499     else static if(is(U: E[], E))
500         enum isWantedType = isWantedType!E;
501     else
502         enum isWantedType = false;
503 }
504 
505 
506 version(testingExcelD) {
507     @("isWorksheetFunction")
508         @safe pure unittest {
509         static import test.d_funcs;
510         // the line below checks that the code still compiles even with a private function
511         // it might stop compiling in a future version when the deprecation rules for
512         // visibility kick in
513         static assert(!isWorksheetFunction!(test.d_funcs.shouldNotBeAProblem));
514         static assert( isWorksheetFunction!(test.d_funcs.FuncThrows));
515         static assert( isWorksheetFunction!(test.d_funcs.DoubleArrayToAnyArray));
516         static assert( isWorksheetFunction!(test.d_funcs.Twice));
517         static assert( isWorksheetFunction!(test.d_funcs.DateTimeToDouble));
518         static assert( isWorksheetFunction!(test.d_funcs.BoolToInt));
519         static assert( isWorksheetFunction!(test.d_funcs.FuncSimpleTupleRet));
520         static assert( isWorksheetFunction!(test.d_funcs.FuncTupleArrayRet));
521         static assert( isWorksheetFunction!(test.d_funcs.FuncDateAndStringRet));
522     }
523 }