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