1 module xlld.wrap;
2 
3 import xlld.xlcall;
4 import xlld.traits: isSupportedFunction;
5 import xlld.framework: FreeXLOper;
6 import xlld.worksheet;
7 import std.traits: isArray;
8 
9 version(unittest) {
10     import unit_threaded;
11 
12     /// emulates SRef types by storing what the referenced type actually is
13     XlType gReferencedType;
14 
15     // tracks calls to `coerce` and `free` to make sure memory allocations/deallocations match
16     int gNumXlCoerce;
17     int gNumXlFree;
18     enum maxCoerce = 1000;
19     void*[maxCoerce] gCoerced;
20     void*[maxCoerce] gFreed;
21 
22     // automatically converts from oper to compare with a D type
23     void shouldEqualDlang(U)(LPXLOPER12 actual, U expected, string file = __FILE__, size_t line = __LINE__) {
24         if(actual.xltype == xltypeErr)
25             fail("XLOPER is of error type", file, line);
26         actual.fromXlOper!U.shouldEqual(expected, file, line);
27     }
28 
29     // automatically converts from oper to compare with a D type
30     void shouldEqualDlang(U)(ref XLOPER12 actual, U expected, string file = __FILE__, size_t line = __LINE__) {
31         shouldEqualDlang(&actual, expected, file, line);
32     }
33 
34     XLOPER12 toSRef(T)(T val) {
35         auto ret = toXlOper(val);
36         //hide real type somewhere to retrieve it
37         gReferencedType = ret.xltype;
38         ret.xltype = XlType.xltypeSRef;
39         return ret;
40     }
41 
42     // tracks allocations and throws in the destructor if there is a memory leak
43     // it also throws when there is an attempt to deallocate memory that wasn't
44     // allocated
45     struct TestAllocator {
46         import std.experimental.allocator.common: platformAlignment;
47         import std.experimental.allocator.mallocator: Mallocator;
48 
49         alias allocator = Mallocator.instance;
50 
51         private static struct ByteRange {
52             void* ptr;
53             size_t length;
54         }
55         private ByteRange[] _allocations;
56         private int _numAllocations;
57 
58         enum uint alignment = platformAlignment;
59 
60         void[] allocate(size_t numBytes) {
61             ++_numAllocations;
62             auto ret = allocator.allocate(numBytes);
63             writelnUt("+ Allocated  ptr ", ret.ptr, " of ", ret.length, " bytes length");
64             _allocations ~= ByteRange(ret.ptr, ret.length);
65             return ret;
66         }
67 
68         bool deallocate(void[] bytes) {
69             import std.algorithm: remove, canFind;
70             import std.exception: enforce;
71             import std.conv: text;
72 
73             writelnUt("- Deallocate ptr ", bytes.ptr, " of ", bytes.length, " bytes length");
74 
75             bool pred(ByteRange other) { return other.ptr == bytes.ptr && other.length == bytes.length; }
76 
77             enforce(_allocations.canFind!pred,
78                     text("Unknown deallocate byte range. Ptr: ", bytes.ptr, " length: ", bytes.length,
79                          " allocations: ", _allocations));
80             _allocations = _allocations.remove!pred;
81             return allocator.deallocate(bytes);
82         }
83 
84         auto numAllocations() @safe pure nothrow const {
85             return _numAllocations;
86         }
87 
88         ~this() {
89             import std.exception: enforce;
90             import std.conv: text;
91             enforce(!_allocations.length, text("Memory leak in TestAllocator. Allocations: ", _allocations));
92         }
93     }
94 }
95 
96 // this shouldn't be needed IMHO and is a bug in std.experimental.allocator that dispose
97 // doesn't handle 2D arrays correctly
98 void dispose(A, T)(auto ref A allocator, T[] array) {
99     static import std.experimental.allocator;
100     import std.traits: Unqual;
101 
102     static if(isArray!T) {
103         foreach(ref e; array) {
104             dispose(allocator, e);
105         }
106     }
107 
108     alias U = Unqual!T;
109     std.experimental.allocator.dispose(allocator, cast(U[])array);
110 }
111 
112 void dispose(A, T)(auto ref A allocator, T value) if(!isArray!T) {
113     static import std.experimental.allocator;
114     std.experimental.allocator.dispose(allocator, value);
115 }
116 
117 XLOPER12 toXlOper(T, A)(T val, ref A allocator) if(is(T == double)) {
118     return toXlOper(val);
119 }
120 
121 XLOPER12 toXlOper(T)(T val) if(is(T == double)) {
122     auto ret = XLOPER12();
123     ret.xltype = XlType.xltypeNum;
124     ret.val.num = val;
125     return ret;
126 }
127 
128 
129 XLOPER12 toXlOper(T)(T val) if(is(T == string)) {
130     import xlld.memorymanager: allocator;
131     return toXlOper(val, allocator);
132 }
133 
134 XLOPER12 toXlOper(T, A)(T val, ref A allocator) if(is(T == string)) {
135     import std.utf: byWchar;
136     import std.stdio;
137 
138     // extra space for the length
139     auto wval = cast(wchar*)allocator.allocate((val.length + 1) * wchar.sizeof).ptr;
140     wval[0] = cast(wchar)val.length;
141 
142     int i = 1;
143     foreach(ch; val.byWchar) {
144         wval[i++] = ch;
145     }
146 
147     auto ret = XLOPER12();
148     ret.xltype = XlType.xltypeStr;
149     ret.val.str = cast(XCHAR*)wval;
150 
151     return ret;
152 }
153 
154 
155 @("toXlOper!string ascii")
156 @system unittest {
157     import std.conv: to;
158 
159     const str = "foo";
160     auto oper = str.toXlOper;
161     scope(exit)FreeXLOper(&oper);
162 
163     oper.xltype.shouldEqual(xltypeStr);
164     (cast(int)oper.val.str[0]).shouldEqual(str.length);
165     (cast(wchar*)oper.val.str)[1 .. str.length + 1].to!string.shouldEqual("foo");
166 }
167 
168 @("toXlOper!string allocator")
169 @system unittest {
170     // should throw unless allocations match deallocations
171     TestAllocator allocator;
172     auto oper = "foo".toXlOper(allocator);
173     allocator.numAllocations.shouldEqual(1);
174     FreeXLOper(&oper, allocator);
175 }
176 
177 XLOPER12 toXlOper(T)(T[][] values) if(is(T == double) || is(T == string))
178 {
179     import xlld.memorymanager: allocator;
180     return toXlOper(values, allocator);
181 }
182 
183 XLOPER12 toXlOper(T, A)(T[][] values, ref A allocator)
184     if(is(T == double) || is(T == string))
185 {
186     import std.algorithm: map, all;
187     import std.array: array;
188     import std.exception: enforce;
189     import std.conv: text;
190 
191     static const exception = new Exception("# of columns must all be the same and aren't");
192     if(!values.all!(a => a.length == values[0].length))
193        throw exception;
194 
195     auto ret = XLOPER12();
196     ret.xltype = XlType.xltypeMulti;
197     const rows = cast(int)values.length;
198     ret.val.array.rows = rows;
199     const cols = cast(int)values[0].length;
200     ret.val.array.columns = cols;
201 
202     ret.val.array.lparray = cast(XLOPER12*)allocator.allocate(rows * cols * ret.sizeof).ptr;
203     auto opers = ret.val.array.lparray[0 .. rows*cols];
204 
205     int i;
206     foreach(ref row; values)
207         foreach(ref val; row) {
208             opers[i++] = val.toXlOper(allocator);
209         }
210 
211     return ret;
212 }
213 
214 
215 @("toXlOper string[][]")
216 @system unittest {
217     auto oper = [["foo", "bar", "baz"], ["toto", "titi", "quux"]].toXlOper;
218     scope(exit) FreeXLOper(&oper);
219 
220     oper.xltype.shouldEqual(xltypeMulti);
221     oper.val.array.rows.shouldEqual(2);
222     oper.val.array.columns.shouldEqual(3);
223     auto opers = oper.val.array.lparray[0 .. oper.val.array.rows * oper.val.array.columns];
224 
225     opers[0].shouldEqualDlang("foo");
226     opers[3].shouldEqualDlang("toto");
227     opers[5].shouldEqualDlang("quux");
228 }
229 
230 @("toXlOper string[][] allocator")
231 @system unittest {
232     TestAllocator allocator;
233     auto oper = [["foo", "bar", "baz"], ["toto", "titi", "quux"]].toXlOper(allocator);
234     allocator.numAllocations.shouldEqual(7);
235     FreeXLOper(&oper, allocator);
236 }
237 
238 @("toXlOper double[][] allocator")
239 @system unittest {
240     TestAllocator allocator;
241     auto oper = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]].toXlOper(allocator);
242     allocator.numAllocations.shouldEqual(1);
243     FreeXLOper(&oper, allocator);
244 }
245 
246 
247 XLOPER12 toXlOper(T)(T values) if(is(T == string[]) || is(T == double[])) {
248     import xlld.memorymanager: allocator;
249     return toXlOper(values, allocator);
250 }
251 
252 XLOPER12 toXlOper(T, A)(T values, ref A allocator) if(is(T == string[]) || is(T == double[])) {
253     T[1] realValues = [values];
254     return realValues.toXlOper(allocator);
255 }
256 
257 
258 @("toXlOper string[] allocator")
259 @system unittest {
260     TestAllocator allocator;
261     auto oper = ["foo", "bar", "baz", "toto", "titi", "quux"].toXlOper(allocator);
262     allocator.numAllocations.shouldEqual(7);
263     FreeXLOper(&oper, allocator);
264 }
265 
266 auto fromXlOper(T)(ref XLOPER12 val) {
267     return (&val).fromXlOper!T;
268 }
269 
270 auto fromXlOper(T, A)(ref XLOPER12 val, ref A allocator) {
271     return (&val).fromXlOper!T(allocator);
272 }
273 
274 
275 auto fromXlOper(T, A)(LPXLOPER12 val, ref A allocator) if(is(T == double)) {
276     return fromXlOper!T(val);
277 }
278 
279 auto fromXlOper(T)(LPXLOPER12 val) if(is(T == double)) {
280     if(val.xltype == xltypeMissing)
281         return double.init;
282 
283     return val.val.num;
284 }
285 
286 @("fromXlOper double allocator")
287 @system unittest {
288     TestAllocator allocator;
289     auto num = 4.0;
290     auto oper = num.toXlOper(allocator);
291     auto back = oper.fromXlOper!double(allocator);
292     back.shouldEqual(num);
293 
294     FreeXLOper(&oper);
295 }
296 
297 
298 @("isNan for fromXlOper!double")
299 @system unittest {
300     import std.math: isNaN;
301     XLOPER12 oper;
302     oper.xltype = XlType.xltypeMissing;
303     fromXlOper!double(&oper).isNaN.shouldBeTrue;
304 }
305 
306 // 2D slices
307 auto fromXlOper(T)(LPXLOPER12 val) if(is(T: E[][], E) && (is(E == string) || is(E == double)))
308 {
309     import xlld.memorymanager: allocator;
310     return fromXlOper!T(val, allocator);
311 }
312 
313 
314 auto fromXlOper(T, A)(LPXLOPER12 val, ref A allocator)
315     if(is(T: E[][], E) && (is(E == string) || is(E == double)))
316 {
317     return val.fromXlOperMulti!(Dimensions.Two, typeof(T.init[0][0]))(allocator);
318 }
319 
320 @("fromXlOper!string[][]")
321 unittest {
322     auto strings = [["foo", "bar", "baz"], ["toto", "titi", "quux"]];
323     auto oper = strings.toXlOper;
324     scope(exit) FreeXLOper(&oper);
325     oper.fromXlOper!(string[][]).shouldEqual(strings);
326 }
327 
328 @("fromXlOper!double[][]")
329 unittest {
330     auto doubles = [[1.0, 2.0], [3.0, 4.0]];
331     auto oper = doubles.toXlOper;
332     scope(exit) FreeXLOper(&oper);
333     oper.fromXlOper!(double[][]).shouldEqual(doubles);
334 }
335 
336 @("fromXlOper!string[][] allocator")
337 unittest {
338     TestAllocator allocator;
339     auto strings = [["foo", "bar", "baz"], ["toto", "titi", "quux"]];
340     auto oper = strings.toXlOper(allocator);
341     auto backAgain = oper.fromXlOper!(string[][])(allocator);
342 
343     allocator.numAllocations.shouldEqual(16);
344 
345     FreeXLOper(&oper, allocator);
346     backAgain.shouldEqual(strings);
347     allocator.dispose(backAgain);
348 }
349 
350 @("fromXlOper!double[][] allocator")
351 unittest {
352     TestAllocator allocator;
353     auto doubles = [[1.0, 2.0], [3.0, 4.0]];
354     auto oper = doubles.toXlOper(allocator);
355     auto backAgain = oper.fromXlOper!(double[][])(allocator);
356 
357     allocator.numAllocations.shouldEqual(4);
358 
359     FreeXLOper(&oper, allocator);
360     backAgain.shouldEqual(doubles);
361     allocator.dispose(backAgain);
362 }
363 
364 
365 private enum Dimensions {
366     One,
367     Two,
368 }
369 
370 // 1D slices
371 auto fromXlOper(T)(LPXLOPER12 val)
372     if(is(T: E[], E) && (is(E == string) || is(E == double)))
373 {
374     import xlld.memorymanager: allocator;
375     return fromXlOper!T(val, allocator);
376 }
377 
378 // 1D slices
379 auto fromXlOper(T, A)(LPXLOPER12 val, ref A allocator)
380     if(is(T: E[], E) && (is(E == string) || is(E == double)))
381 {
382     return val.fromXlOperMulti!(Dimensions.One, typeof(T.init[0]))(allocator);
383 }
384 
385 
386 @("fromXlOper!string[]")
387 unittest {
388     auto strings = ["foo", "bar", "baz", "toto", "titi", "quux"];
389     auto oper = strings.toXlOper;
390     scope(exit) FreeXLOper(&oper);
391     oper.fromXlOper!(string[]).shouldEqual(strings);
392 }
393 
394 @("fromXlOper!double[]")
395 unittest {
396     auto doubles = [1.0, 2.0, 3.0, 4.0];
397     auto oper = doubles.toXlOper;
398     scope(exit) FreeXLOper(&oper);
399     oper.fromXlOper!(double[]).shouldEqual(doubles);
400 }
401 
402 @("fromXlOper!string[] allocator")
403 unittest {
404     TestAllocator allocator;
405     auto strings = ["foo", "bar", "baz", "toto", "titi", "quux"];
406     auto oper = strings.toXlOper(allocator);
407     auto backAgain = oper.fromXlOper!(string[])(allocator);
408 
409     allocator.numAllocations.shouldEqual(14);
410 
411     backAgain.shouldEqual(strings);
412     FreeXLOper(&oper, allocator);
413     allocator.dispose(backAgain);
414 }
415 
416 @("fromXlOper!double[] allocator")
417 unittest {
418     TestAllocator allocator;
419     auto doubles = [1.0, 2.0, 3.0, 4.0];
420     auto oper = doubles.toXlOper(allocator);
421     auto backAgain = oper.fromXlOper!(double[])(allocator);
422 
423     allocator.numAllocations.shouldEqual(2);
424 
425     backAgain.shouldEqual(doubles);
426     FreeXLOper(&oper, allocator);
427     allocator.dispose(backAgain);
428 }
429 
430 
431 private auto fromXlOperMulti(Dimensions dim, T, A)(LPXLOPER12 val, ref A allocator) {
432     import xlld.xl: coerce, free;
433     import std.exception: enforce;
434     import std.experimental.allocator: makeArray;
435 
436     static const exception = new Exception("XL oper not of multi type");
437 
438     const realType = val.xltype & ~xlbitDLLFree;
439     if(realType != xltypeMulti)
440         throw exception;
441 
442     const rows = val.val.array.rows;
443     const cols = val.val.array.columns;
444 
445     static if(dim == Dimensions.Two) {
446         auto ret = allocator.makeArray!(T[])(rows);
447         foreach(ref row; ret)
448             row = allocator.makeArray!T(cols);
449     } else static if(dim == Dimensions.One) {
450 
451         auto ret = allocator.makeArray!T(rows * cols);
452     } else
453         static assert(0);
454 
455     auto values = val.val.array.lparray[0 .. (rows * cols)];
456 
457     foreach(const row; 0 .. rows) {
458         foreach(const col; 0 .. cols) {
459             auto cellVal = coerce(&values[row * cols + col]);
460             scope(exit) free(&cellVal);
461 
462             auto value = cellVal.xltype == dlangToXlOperType!T.Type ? cellVal.fromXlOper!T(allocator) : T.init;
463             static if(dim == Dimensions.Two)
464                 ret[row][col] = value;
465             else
466                 ret[row * cols + col] = value;
467         }
468     }
469 
470     return ret;
471 }
472 
473 
474 auto fromXlOper(T)(LPXLOPER12 val) if(is(T == string)) {
475     import xlld.memorymanager: allocator;
476     return fromXlOper!T(val, allocator);
477 }
478 
479 auto fromXlOper(T, A)(LPXLOPER12 val, ref A allocator) if(is(T == string)) {
480 
481     import std.experimental.allocator: makeArray;
482     import std.utf;
483 
484     const stripType = val.xltype & ~(xlbitXLFree | xlbitDLLFree);
485     if(stripType != xltypeStr)
486         return null;
487 
488     auto ret = allocator.makeArray!char(val.val.str[0]);
489     int i;
490     foreach(ch; val.val.str[1 .. ret.length + 1].byChar)
491         ret[i++] = ch;
492 
493     return cast(string)ret;
494 }
495 
496 @("fromXlOper missing")
497 @system unittest {
498     XLOPER12 oper;
499     oper.xltype = XlType.xltypeMissing;
500     fromXlOper!string(&oper).shouldBeNull;
501 }
502 
503 @("fromXlOper string allocator")
504 @system unittest {
505     TestAllocator allocator;
506     auto oper = "foo".toXlOper(allocator);
507     auto str = fromXlOper!string(&oper, allocator);
508     allocator.numAllocations.shouldEqual(2);
509 
510     FreeXLOper(&oper, allocator);
511     str.shouldEqual("foo");
512     allocator.dispose(cast(void[])str);
513 }
514 
515 private enum isWorksheetFunction(alias F) =
516     isSupportedFunction!(F, double, double[][], string[][], string[], double[], string);
517 
518 @safe pure unittest {
519     import xlld.test_d_funcs;
520     // the line below checks that the code still compiles even with a private function
521     // it might stop compiling in a future version when the deprecation rules for
522     // visibility kick in
523     static assert(!isWorksheetFunction!shouldNotBeAProblem);
524     static assert(!isWorksheetFunction!FuncThrows);
525 }
526 
527 /**
528    A string to mixin that wraps all eligible functions in the
529    given module.
530  */
531 string wrapModuleWorksheetFunctionsString(string moduleName)() {
532     if(!__ctfe) {
533         return "";
534     }
535 
536     import xlld.traits: Identity;
537     import std.array: join;
538     import std.traits: ReturnType, Parameters;
539 
540     mixin(`import ` ~ moduleName ~ `;`);
541     alias module_ = Identity!(mixin(moduleName));
542 
543     string ret = `static import ` ~ moduleName ~ ";\n\n";
544 
545     foreach(moduleMemberStr; __traits(allMembers, module_)) {
546         alias moduleMember = Identity!(__traits(getMember, module_, moduleMemberStr));
547 
548         static if(isWorksheetFunction!moduleMember) {
549             ret ~= wrapModuleFunctionStr!(moduleName, moduleMemberStr);
550         }
551     }
552 
553     return ret;
554 }
555 
556 
557 @("Wrap double[][] -> double")
558 @system unittest {
559     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
560 
561     auto arg = toSRef(cast(double[][])[[1, 2, 3, 4], [11, 12, 13, 14]]);
562     FuncAddEverything(&arg).shouldEqualDlang(60.0);
563 
564     arg = toSRef(cast(double[][])[[0, 1, 2, 3], [4, 5, 6, 7]]);
565     FuncAddEverything(&arg).shouldEqualDlang(28.0);
566 }
567 
568 @("Wrap double[][] -> double[][]")
569 @system unittest {
570     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
571 
572     auto arg = toSRef(cast(double[][])[[1, 2, 3, 4], [11, 12, 13, 14]]);
573     FuncTripleEverything(&arg).shouldEqualDlang(cast(double[][])[[3, 6, 9, 12], [33, 36, 39, 42]]);
574 
575     arg = toSRef(cast(double[][])[[0, 1, 2, 3], [4, 5, 6, 7]]);
576     FuncTripleEverything(&arg).shouldEqualDlang(cast(double[][])[[0, 3, 6, 9], [12, 15, 18, 21]]);
577 }
578 
579 
580 @("Wrap string[][] -> double")
581 @system unittest {
582     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
583 
584     auto arg = toSRef([["foo", "bar", "baz", "quux"], ["toto", "titi", "tutu", "tete"]]);
585     FuncAllLengths(&arg).shouldEqualDlang(29.0);
586 
587     arg = toSRef([["", "", "", ""], ["", "", "", ""]]);
588     FuncAllLengths(&arg).shouldEqualDlang(0.0);
589 }
590 
591 @("Wrap string[][] -> double[][]")
592 @system unittest {
593     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
594 
595     auto arg = toSRef([["foo", "bar", "baz", "quux"], ["toto", "titi", "tutu", "tete"]]);
596     FuncLengths(&arg).shouldEqualDlang(cast(double[][])[[3, 3, 3, 4], [4, 4, 4, 4]]);
597 
598     arg = toSRef([["", "", ""], ["", "", "huh"]]);
599     FuncLengths(&arg).shouldEqualDlang(cast(double[][])[[0, 0, 0], [0, 0, 3]]);
600 }
601 
602 @("Wrap string[][] -> string[][]")
603 @system unittest {
604     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
605 
606     auto arg = toSRef([["foo", "bar", "baz", "quux"], ["toto", "titi", "tutu", "tete"]]);
607     FuncBob(&arg).shouldEqualDlang([["foobob", "barbob", "bazbob", "quuxbob"],
608                                     ["totobob", "titibob", "tutubob", "tetebob"]]);
609 }
610 
611 @("Wrap string[] -> double")
612 @system unittest {
613     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
614     auto arg = toSRef([["foo", "bar"], ["baz", "quux"]]);
615     FuncStringSlice(&arg).shouldEqualDlang(4.0);
616 }
617 
618 @("Wrap double[] -> double")
619 @system unittest {
620     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
621     auto arg = toSRef([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
622     FuncDoubleSlice(&arg).shouldEqualDlang(6.0);
623 }
624 
625 @("Wrap double[] -> double[]")
626 @system unittest {
627     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
628     auto arg = toSRef([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
629     FuncSliceTimes3(&arg).shouldEqualDlang([3.0, 6.0, 9.0, 12.0, 15.0, 18.0]);
630 }
631 
632 @("Wrap string[] -> string[]")
633 @system unittest {
634     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
635     auto arg = toSRef(["quux", "toto"]);
636     StringsToStrings(&arg).shouldEqualDlang(["quuxfoo", "totofoo"]);
637 }
638 
639 @("Wrap string[] -> string")
640 @system unittest {
641     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
642     auto arg = toSRef(["quux", "toto"]);
643     StringsToString(&arg).shouldEqualDlang("quux, toto");
644 }
645 
646 @("Wrap string -> string")
647 @system unittest {
648     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
649     auto arg = toXlOper("foo");
650     StringToString(&arg).shouldEqualDlang("foobar");
651 }
652 
653 @("Wrap string, string, string -> string")
654 @system unittest {
655     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
656     auto arg0 = toXlOper("foo");
657     auto arg1 = toXlOper("bar");
658     auto arg2 = toXlOper("baz");
659     ManyToString(&arg0, &arg1, &arg2).shouldEqualDlang("foobarbaz");
660 }
661 
662 @("Only look at nothrow functions")
663 @system unittest {
664     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
665     auto arg = toXlOper(2.0);
666     static assert(!__traits(compiles, FuncThrows(&arg)));
667 }
668 
669 @("FuncAddEverything wrapper is @nogc")
670 @system @nogc unittest {
671     import std.experimental.allocator.mallocator: Mallocator;
672 
673     mixin(wrapModuleWorksheetFunctionsString!"xlld.test_d_funcs");
674     auto arg = toXlOper(2.0, Mallocator.instance);
675     scope(exit) FreeXLOper(&arg, Mallocator.instance);
676     FuncAddEverything(&arg);
677 }
678 
679 private enum invalidXlOperType = 0xdeadbeef;
680 
681 /**
682  Maps a D type to two integer xltypes from XLOPER12.
683  InputType is the type actually passed in by the spreadsheet,
684  whilst Type is the Type that it gets coerced to.
685  */
686 template dlangToXlOperType(T) {
687     static if(is(T == double[][]) || is(T == string[][]) || is(T == double[]) || is(T == string[])) {
688         enum InputType = xltypeSRef;
689         enum Type= xltypeMulti;
690     } else static if(is(T == double)) {
691         enum InputType = xltypeNum;
692         enum Type = xltypeNum;
693     } else static if(is(T == string)) {
694         enum InputType = xltypeStr;
695         enum Type = xltypeStr;
696     } else {
697         enum InputType = invalidXlOperType;
698         enum Type = invalidXlOperType;
699     }
700 }
701 
702 /**
703  A string to use with `mixin` that wraps a D function
704  */
705 string wrapModuleFunctionStr(string moduleName, string funcName)() {
706     if(!__ctfe) {
707         return "";
708     }
709 
710     import std.array: join;
711     import std.traits: Parameters, functionAttributes, FunctionAttribute, getUDAs;
712     import std.conv: to;
713     import std.algorithm: map;
714     import std.range: iota;
715     mixin("import " ~ moduleName ~ ": " ~ funcName ~ ";");
716 
717     const argsLength = Parameters!(mixin(funcName)).length;
718     // e.g. LPXLOPER12 arg0, LPXLOPER12 arg1, ...
719     const argsDecl = argsLength.iota.map!(a => `LPXLOPER12 arg` ~ a.to!string).join(", ");
720     // e.g. arg0, arg1, ...
721     const argsCall = argsLength.iota.map!(a => `arg` ~ a.to!string).join(", ");
722     const nogc = functionAttributes!(mixin(funcName)) & FunctionAttribute.nogc
723         ? "@nogc "
724         : "";
725 
726     alias registerAttrs = getUDAs!(mixin(funcName), Register);
727     static assert(registerAttrs.length == 0 || registerAttrs.length == 1,
728                   "Invalid number of @Register on " ~ funcName);
729 
730     string register;
731     static if(registerAttrs.length)
732         register = `@` ~ registerAttrs[0].to!string;
733 
734     return [
735         register,
736         `extern(Windows) LPXLOPER12 ` ~ funcName ~ `(` ~ argsDecl ~ `) nothrow ` ~ nogc ~ `{`,
737         `    static import ` ~ moduleName ~ `;`,
738         `    alias wrappedFunc = ` ~ moduleName ~ `.` ~ funcName ~ `;`,
739         `    return wrapModuleFunctionImpl!wrappedFunc(` ~ argsCall ~  `);`,
740         `}`,
741     ].join("\n");
742 }
743 
744 @system unittest {
745     import xlld.worksheet;
746     import std.traits: getUDAs;
747 
748     mixin(wrapModuleFunctionStr!("xlld.test_d_funcs", "FuncAddEverything"));
749     alias registerAttrs = getUDAs!(FuncAddEverything, Register);
750     static assert(registerAttrs[0].argumentText.value == "Array to add");
751 }
752 
753 /**
754  Implement a wrapper for a regular D function
755  */
756 LPXLOPER12 wrapModuleFunctionImpl(alias wrappedFunc, T...)(T args) {
757     import xlld.memorymanager: allocator;
758     return wrapModuleFunctionImplAllocator!wrappedFunc(allocator, args);
759 }
760 
761 LPXLOPER12 wrapModuleFunctionImplAllocator(alias wrappedFunc, A, T...)
762                                           (ref A allocator, T args) {
763     import xlld.xl: free;
764     import std.traits: Parameters;
765     import std.typecons: Tuple;
766 
767     static XLOPER12 ret;
768 
769     XLOPER12[T.length] realArgs;
770     // must 1st convert each argument to the "real" type.
771     // 2D arrays are passed in as SRefs, for instance
772     foreach(i, InputType; Parameters!wrappedFunc) {
773         if(args[i].xltype == xltypeMissing) {
774              realArgs[i] = *args[i];
775              continue;
776         }
777         try
778             realArgs[i] = convertInput!InputType(args[i]);
779         catch(Exception ex) {
780             ret.xltype = XlType.xltypeErr;
781             ret.val.err = -1;
782             return &ret;
783         }
784     }
785 
786     scope(exit)
787         foreach(ref arg; realArgs)
788             free(&arg);
789 
790     Tuple!(Parameters!wrappedFunc) dArgs; // the D types to pass to the wrapped function
791 
792     // next call the wrapped function with D types
793     foreach(i, InputType; Parameters!wrappedFunc) {
794         try {
795             dArgs[i] = fromXlOper!InputType(&realArgs[i], allocator);
796         } catch(Exception ex) {
797             ret.xltype = XlType.xltypeErr;
798             ret.val.err = -1;
799             return &ret;
800         }
801     }
802 
803     try
804         ret = toXlOper(wrappedFunc(dArgs.expand), allocator);
805     catch(Exception ex)
806         return null;
807 
808     foreach(ref dArg; dArgs) {
809         import std.traits: isPointer;
810         static if(isArray!(typeof(dArg)) || isPointer!(typeof(dArg)))
811             allocator.dispose(dArg);
812     }
813 
814     ret.xltype |= xlbitDLLFree;
815 
816     return &ret;
817 }
818 
819 @("No memory allocation bugs in wrapModuleFunctionImplAllocator for double return")
820 @system unittest {
821     import xlld.test_d_funcs: FuncAddEverything;
822 
823     TestAllocator allocator;
824     auto arg = toSRef([1.0, 2.0]);
825     auto oper = wrapModuleFunctionImplAllocator!FuncAddEverything(allocator, &arg);
826     (oper.xltype & xlbitDLLFree).shouldBeTrue;
827     allocator.numAllocations.shouldEqual(2);
828     oper.shouldEqualDlang(3.0);
829     FreeXLOper(oper, allocator); // normally this is done by Excel
830 }
831 
832 @("No memory allocation bugs in wrapModuleFunctionImplAllocator for double[][] return")
833 @system unittest {
834     import xlld.test_d_funcs: FuncTripleEverything;
835 
836     TestAllocator allocator;
837     auto arg = toSRef([1.0, 2.0, 3.0]);
838     auto oper = wrapModuleFunctionImplAllocator!FuncTripleEverything(allocator, &arg);
839     (oper.xltype & xlbitDLLFree).shouldBeTrue;
840     (oper.xltype & ~xlbitDLLFree).shouldEqual(xltypeMulti);
841     allocator.numAllocations.shouldEqual(3);
842     oper.shouldEqualDlang([[3.0, 6.0, 9.0]]);
843     FreeXLOper(oper, allocator); // normally this is done by Excel
844 }
845 
846 string wrapWorksheetFunctionsString(Modules...)() {
847 
848     if(!__ctfe) {
849         return "";
850     }
851 
852     string ret;
853     foreach(module_; Modules) {
854         ret ~= wrapModuleWorksheetFunctionsString!module_;
855     }
856 
857     return ret;
858 }
859 
860 
861 string wrapAll(Modules...)(in string mainModule = __MODULE__) {
862 
863     if(!__ctfe) {
864         return "";
865     }
866 
867     import xlld.traits: implGetWorksheetFunctionsString;
868     return
869         wrapWorksheetFunctionsString!Modules ~
870         "\n" ~
871         implGetWorksheetFunctionsString!(mainModule) ~
872         "\n" ~
873         `mixin GenerateDllDef!"` ~ mainModule ~ `";` ~
874         "\n";
875 }
876 
877 @("wrapAll")
878 unittest  {
879     import xlld.traits: getAllWorksheetFunctions, GenerateDllDef;
880     mixin(wrapAll!("xlld.test_d_funcs"));
881     auto arg = toSRef(cast(double[][])[[1, 2, 3, 4], [11, 12, 13, 14]]);
882     FuncAddEverything(&arg).shouldEqualDlang(60.0);
883 }
884 
885 
886 XLOPER12 convertInput(T)(LPXLOPER12 arg) {
887     import xlld.xl: coerce, free;
888 
889     static exception = new const Exception("Error converting input");
890 
891     if(arg.xltype != dlangToXlOperType!T.InputType)
892         throw exception;
893 
894     auto realArg = coerce(arg);
895 
896     if(realArg.xltype != dlangToXlOperType!T.Type) {
897         free(&realArg);
898         throw exception;
899     }
900 
901     return realArg;
902 }
903 
904 /**
905   creates an XLOPER12 that can be returned to Excel which
906   will be freed by Excel itself
907  */
908 XLOPER12 toAutoFreeOper(T)(T value) {
909     import xlld.memorymanager: autoFreeAllocator = allocator;
910     import xlld.xlcall: XlType;
911 
912     auto result = value.toXlOper(autoFreeAllocator);
913     result.xltype |= XlType.xlbitDLLFree;
914     return result;
915 }
916 
917 ushort operStringLength(T)(T value) {
918     import std.exception: enforce;
919     import std.conv: text;
920 
921     enforce(value.xltype == xltypeStr,
922             text("Cannot calculate string length for oper of type ", value.xltype));
923 
924     return cast(ushort)value.val.str[0];
925 }
926 
927 auto fromXlOperCoerce(T)(LPXLOPER12 val) {
928     return fromXlOperCoerce(*val);
929 }
930 
931 auto fromXlOperCoerce(T, A)(LPXLOPER12 val, auto ref A allocator) {
932     return fromXlOperCoerce!T(*val, allocator);
933 }
934 
935 
936 auto fromXlOperCoerce(T)(ref XLOPER12 val) {
937     import xlld.memorymanager: allocator;
938     return fromXlOperCoerce!T(val, allocator);
939 }
940 
941 
942 auto fromXlOperCoerce(T, A)(ref XLOPER12 val, auto ref A allocator) {
943     import std.experimental.allocator: dispose;
944     import xlld.xl: coerce, free;
945 
946     auto coerced = coerce(&val);
947     scope(exit) free(&coerced);
948 
949     return coerced.fromXlOper!T(allocator);
950 }
951 
952 
953 @("fromXlOperCoerce")
954 unittest {
955     double[][] doubles = [[1, 2, 3, 4], [11, 12, 13, 14]];
956     auto doublesOper = toSRef(doubles);
957     doublesOper.fromXlOper!(double[][]).shouldThrowWithMessage("XL oper not of multi type");
958     doublesOper.fromXlOperCoerce!(double[][]).shouldEqual(doubles);
959 }