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