1 
2 //          Copyright Marcelo S. N. Mancini(Hipreme) 2020.
3 // Distributed under the Boost Software License, Version 1.0.
4 //    (See accompanying file LICENSE_1_0.txt or copy at
5 //          http://www.boost.org/LICENSE_1_0.txt)
6 
7 module generate;
8 import regexes;
9 import std.file;
10 import std.process:executeShell;
11 import std.array;
12 import std..string;
13 import std.getopt;
14 import std.regex : replaceAll, matchAll, regex, Regex;
15 import std.path:baseName, stripExtension;
16 import std.stdio:writeln, File;
17 import pluginadapter;
18 import plugin;
19 
20 
21 enum D_TO_REPLACE
22 {
23     loneVoid = "()",    
24     unsigned_int = "uint",
25     unsigned_char = "ubyte",
26     _string = "const (char)*", //This will be on the final as it causes problem on regexes
27     head_const = "const $1",
28     _callback = "$1 function($3) $2",
29     _in = " in_",
30     _out = " out_",
31     _align = " align_",
32     _ref = " ref_",
33     //C++ part
34     _template = "$2!($1)",
35     address = "ref $1",
36     NULL = " null",
37 
38     _struct = "",
39     _array = "$1* $2",
40     _nullAddress = " null"
41 }
42 
43 enum AliasCreation = "alias p$2 = $1 function";
44 enum GSharedCreation = "p$2 $2";
45 enum BindSymbolCreation = "lib.bindSymbol(cast(void**)&$2, \"$2\");";
46 
47 
48 
49 File createDppFile(string file)
50 {
51     File f;
52     string dppFile = baseName(stripExtension(file)) ~ ".dpp";
53     if(!exists(file))
54     {
55         writeln("File does not exists");
56         return f;
57     }
58     if(lastIndexOf(file, ".h") == -1)
59     {
60         writeln("File must be a header");
61         return f;
62     }
63     if(!exists(dppFile))
64     {
65         f = File(dppFile, "w");
66         f.write("#include \""~file~"\"");
67         writeln("File '" ~ dppFile ~ "' created");
68     }
69     else
70     {
71         f = File(dppFile);
72         writeln("File '" ~ dppFile ~ "' already exists, ignoring content creation");
73     }
74     return f;
75 }
76 
77 bool executeDpp(File file, string _dppArgs)
78 {
79     string[4] tests = ["d++", "d++.exe", "dpp", "dpp.exe"];
80     string selected;
81     foreach(t; tests)
82     {
83         if(exists(t))
84         {
85             selected = t;
86             break;
87         }
88     }
89     if(selected == "")
90     {
91         writeln("Could not create types.d\nReason: d++/dpp is not on the current folder");
92         return false;
93     }
94 
95     string[] dppArgs = [selected, "--preprocess-only"];
96     if(_dppArgs != "")
97         dppArgs~= _dppArgs.split(",");
98     dppArgs~=file.name;
99 
100     auto ret = executeShell(dppArgs.join(" "));
101     
102 
103     //Okay
104     if(ret.status == 0)
105     {
106         writeln("Types.d was succesfully created with "~dppArgs.join(" "));
107         //Instead of renaming, just copy its content and delete it
108         string noExt = stripExtension(file.name);
109         string genName = noExt~".d";
110         string fileContent = "module bindbc."~ noExt~".types;\n" ~readText(genName);
111         mkdirRecurse("bindbc/cimgui");
112         std.file.write("bindbc/"~noExt~"/types.d", fileContent);
113     }
114     else
115     {
116         writeln(r"
117 Could not execute dpp:
118 DPP output
119 --------------------------------------------------
120 ");
121     writeln(ret.output);
122     writeln(r"
123 --------------------------------------------------
124 End of Dpp output
125 ");
126     }
127     return true;
128 }
129 
130 /**
131 *   Get every function from file following the regex, the regex should only match
132 */
133 auto getFuncs(Input, Reg)(Input file, Reg reg)
134 {
135     if(lastIndexOf(file, ".h") == -1)
136         return "";
137 
138     writeln("Getting file '"~file~"' functions");
139     string f = readText(file);
140     auto matches = matchAll(f, reg);
141     string ret;
142     foreach(m; matches)
143     {
144         ret~= m.hit~"\n";
145     }
146     return ret;
147 }
148 
149 /**
150 *   Uses a bunch of presets written in the file head, it will convert every C func
151 * declaration to D, arrays are transformed to pointers, as if it becomes ref, the function
152 * won't be able to accept casts
153 */
154 string cppFuncsToD(string funcs, bool replaceAditional = false)
155 {
156     alias f = funcs;
157     writeln("Converting functions to D style");
158     with(D_TO_REPLACE)
159     {
160         f = f.replaceAll(CPP_TO_D.replaceUint, unsigned_int);
161         f = f.replaceAll(CPP_TO_D.replaceUByte, unsigned_char);
162         f = f.replaceAll(CPP_TO_D.replaceCallback, _callback);
163         f = f.replaceAll(CPP_TO_D.replaceIn, _in);
164         f = f.replaceAll(CPP_TO_D.replaceOut, _out);
165         f = f.replaceAll(CPP_TO_D.replaceAlign, _align);
166         f = f.replaceAll(CPP_TO_D.replaceRef, _ref);
167         //C++ Part
168         f = f.replaceAll(CPP_TO_D.replaceTemplate, _template);
169         f = f.replaceAll(CPP_TO_D.replaceAddress, address);
170         f = f.replaceAll(CPP_TO_D.replaceNULL, NULL);
171 
172         f = f.replaceAll(CPP_TO_D.replaceStruct, _struct);
173         f = f.replaceAll(CPP_TO_D.replaceArray, _array);
174         f = f.replaceAll(CPP_TO_D.replaceNullAddress, _nullAddress);
175         f = f.replaceAll(CPP_TO_D.removeLoneVoid, loneVoid );
176         
177         if(replaceAditional)
178         {
179             f = f.replaceAll(CPP_TO_D.replaceString, _string);
180             f = f.replaceAll(CPP_TO_D.replaceHeadConst, head_const);
181         }
182     }
183     return funcs;
184 }
185 
186 auto cleanPreFuncsDeclaration(Strs, Reg)(Strs funcs, Reg reg)
187 {
188     writeln("Cleaning pre function declaration");
189     funcs = replaceAll(funcs, reg, "$1");
190     return funcs;
191 }
192 
193 string[] getFuncNames(string[] funcs)
194 {
195     writeln("Getting function names");
196     foreach(ref f; funcs)
197         f = f.replaceAll(GetFuncParamsAndName2, "$2");
198     return funcs;
199 }
200 
201 string generateAliases(string[] funcs)
202 {
203     string ret = "";
204     foreach(f; funcs)
205     {
206         string buf = f.replaceAll(GetFuncParamsAndName2, "alias da_$2 = $1 function $3;\n\t"); 
207         buf = buf.replaceAll(CPP_TO_D.replaceString, D_TO_REPLACE._string);
208         buf = buf.replaceAll(CPP_TO_D.replaceHeadConst, D_TO_REPLACE.head_const);
209         ret~=buf;
210     }
211     return ret;
212 }
213 
214 string generateGSharedFuncs(string[] funcs)
215 {
216     string ret = "\n__gshared\n{\t";
217 
218     size_t len = funcs.length-1;
219     foreach(i, f; funcs)
220     {
221         if(f != "")
222         {
223             ret~= "da_"~f~" "~f~";\n";
224             if(i + 1 != len)
225                 ret~="\t";
226         }
227     }
228 
229     ret~="}";
230     return ret;
231 }
232 
233 void createFuncsFile(string libName, ref string[] funcNames)
234 {
235     writeln("Writing funcs.d");
236     string fileContent = q{
237 module bindbc.$.funcs;
238 import bindbc.$.types;
239 import core.stdc.stdarg:va_list;
240 
241 extern(C) @nogc nothrow
242 }.replaceAll(DollarToLib, libName);
243     fileContent~="{\n\t";
244     fileContent~=generateAliases(funcNames);
245     fileContent~="\n}";
246 
247     funcNames = getFuncNames(funcNames);
248     fileContent~=generateGSharedFuncs(funcNames);
249     
250     mkdirRecurse("bindbc/"~libName);
251     std.file.write("./bindbc/"~libName~"/funcs.d", fileContent);
252 }
253 
254 /**
255 *   
256 */
257 string generateBindSymbols(string[] funcs)
258 {
259     string ret = "";
260     size_t len = funcs.length-1;
261     foreach(i, f; funcs)
262     {
263         if(f != "")
264             ret~= "lib.bindSymbol(cast(void**)&"~f~", \""~f~"\");\n";
265         if(i + 1 != len)
266             ret~="\t";
267     }
268     return ret;
269 }
270 
271 
272 /**
273 *   Create library loading file named libNameload.d
274 */
275 void createLibLoad(string libName, string[] funcNames)
276 {
277     writeln("Writing "~libName~"load.d");
278     string fileContent = "module bindbc."~libName~"."~libName~"load;\n";
279     fileContent~="import bindbc.loader;\n";
280     fileContent~="import bindbc."~libName~".types;\n";
281     fileContent~="import bindbc."~libName~".funcs;\n";
282     fileContent~="private\n{\n\tSharedLib lib;\n}";
283     fileContent~=`
284 bool load$()
285 {
286     version(Windows){
287         const (char)[][1] libNames = ["$.dll"];
288     }
289     else version(OSX){
290         const(char)[][7] libNames = [
291         "lib$.dylib",
292         "/usr/local/lib/lib$.dylib",
293         "/usr/local/lib/lib$/lib$.dylib",
294         "../Frameworks/$.framework/$",
295         "/Library/Frameworks/$.framework/$",
296         "/System/Library/Frameworks/$.framework/$",
297         "/opt/local/lib/lib$.dylib"
298         ];
299     }
300     else version(Posix){
301         const(char)[][8] libNames = [
302         "$.so",
303         "/usr/local/lib/$.so",
304         "$.so.1",
305         "/usr/local/lib/$.so.1",
306         "lib$.so",
307         "/usr/local/lib/lib$.so",
308         "lib$.so.1",
309         "/usr/local/lib/lib$.so.1"
310         ];  
311     }
312     else static assert(0, "bindbc-$ is not yet supported on this platform.");
313     foreach(name; libNames) 
314     {
315         lib = load(name.ptr);
316         if(lib != invalidHandle)
317             return _load();
318     }
319     return false;
320 }`;
321 // };//Token strings for some reason seems to be jumping more lines than the expected
322     fileContent~=r"
323 private bool _load()
324 {
325     bool isOkay = true;
326     import std.stdio:writeln;
327     const size_t errs = errorCount();
328     loadSymbols();
329     if(errs != errorCount())
330     {
331         isOkay = false;
332         import std.conv:to;
333         foreach(err; errors)
334         {
335             writeln(to!string(err.message));
336         }
337     }
338     return isOkay;
339 }";
340 // };
341     fileContent = replaceAll(fileContent, DollarToLib, libName);
342     fileContent~="private void loadSymbols()\n{\n\t";
343     fileContent~= generateBindSymbols(funcNames);
344     fileContent~="}";
345 
346     File _f = File("bindbc/"~libName~"/"~libName~"load.d", "w");
347     _f.write(fileContent);
348     _f.close();
349 }
350 
351 void createPackage(string libName)
352 {
353     //No need to regenerate it everytime
354     if(exists("bindbc/"~libName~"/package.d"))
355         return;
356     string fileContent = q{
357 module bindbc.$;
358 
359 public import bindbc.$.funcs;
360 public import bindbc.$.$load;
361 public import bindbc.$.types;
362 }.replaceAll(DollarToLib, libName);
363     std.file.write("bindbc/"~libName~"/package.d", fileContent);
364 }
365 
366 
367 enum ERROR = -1;
368 
369 string optFile;
370 string optDppArgs;
371 string optPresets;
372 bool optNoTypes;
373 bool optLoad;
374 string optCustom;
375 bool optFuncPrefix;
376 string[] optUsingPlugins = [];
377 bool optLoadAll;
378 string[][string] optPluginArgs;
379 bool optRecompile;
380 bool optDebug;
381 
382 
383 enum ReservedArgs : string
384 {
385     D_CONV = "d-conv"
386 }
387 /**
388 *   Remove d-conv from the args and sets willConvertToD to true
389 */
390 void getDConvPlugins()
391 {
392     import std.algorithm : countUntil;
393     foreach(pluginName, args; optPluginArgs)
394     {
395         long ind = countUntil(args, ReservedArgs.D_CONV);
396         if(ind != -1)
397         {
398             if(ind != args.length)
399                 optPluginArgs[pluginName] = args[0..ind] ~ args[ind+1..$];
400             else
401                 optPluginArgs[pluginName] = args[0..ind];
402             PluginAdapter.loadedPlugins[pluginName].willConvertToD = true;
403         }
404     }
405 }
406 
407 void pluginArgsHandler(string opt, string value)
408 {
409     import std.array:split;
410     import std.algorithm:countUntil;
411     if(opt == "plugin-args|a")
412     {
413         if(value.countUntil("=") == -1)
414             return writeln("plugin-args wrong formatting!");
415         string[] v = value.split("=");
416         string pluginName = v[0];
417         if(v[1].countUntil("[") != -1 && v[1].countUntil("]") == v[1].length - 1)
418             optPluginArgs[pluginName]~= v[1][1..$-1].split(" ");
419         else
420             optPluginArgs[pluginName]~= v[1];
421     }
422 }
423 
424 void playPlugins(string cwd)
425 {
426     foreach(pluginName, pluginArgs; optPluginArgs)
427     {
428         Plugin p = PluginAdapter.loadedPlugins[pluginName];
429         writeln("'", pluginName, "' under execution with arguments ", cwd~pluginArgs, "\n\n\n");
430         int retVal = p.main(cwd ~ pluginArgs);
431         if(retVal == Plugin.SUCCESS)
432         {
433             string processed = p.convertToD_Pipe();
434             if(p.willConvertToD)
435                 processed= cppFuncsToD(processed, true);
436             if(p.onReturnControl(processed) == Plugin.ERROR)
437                 goto PLUGIN_ERROR;
438             writeln("'", pluginName, "' finished tasks.\n\n\n");
439             p.hasFinishedExecution = true;
440         }
441         else
442         {
443             PLUGIN_ERROR: writeln("Error ocurred while executing '", pluginName, "'!\n\t->", p.error);
444         }
445     }
446 }
447 
448 void checkPresets(ref Regex!char targetReg)
449 {
450     if(optPresets != "")
451     {
452         switch(optPresets)
453         {
454             case "cimgui":
455                 targetReg = Presets.cimguiFuncs;
456                 optDppArgs = "--parse-as-cpp,--define CIMGUI_DEFINE_ENUMS_AND_STRUCTS";
457                 if(optFile == "")
458                     optFile = "cimgui.h";
459                 break;
460             default:
461                 writeln("Preset named '"~optPresets~"' does not exists");
462                 break;
463         }
464     }
465 }
466 
467 void checkCustomRegex(ref Regex!char targetReg)
468 {
469     if(optPresets == "" && optCustom != "")
470     {
471         writeln("
472 Please consider adding your custom function getter to the list.
473 Just create an issue or a pull request on https://www.github.com/MrcSnm/bindbc-generator
474 ");
475         string reg;
476         if(optFuncPrefix)
477         {
478             reg~=r"^(?:";
479             reg~=optCustom;
480             reg~=r")(.+\);)$";
481         }
482         else
483             reg~= optCustom;
484         writeln("Compiling regex");
485         targetReg = regex(reg, "mg"); //Auto converts single slash to double for easier usage
486         writeln("Regex Generated:\n\t"~targetReg.toString);
487     }
488 }
489 
490 void helpInfoSetup(ref GetoptResult helpInfo)
491 {
492     //Dpparg
493     helpInfo.options[0].help = "Arguments to be appended to dpp, --preprocess-only is always included. Pass multiple arguments via comma";
494     //File
495     helpInfo.options[1].help = "Target header to get functions and types for generation";
496     //Presets
497     helpInfo.options[2].help = r"
498 (Presets and custom are mutually exclusive)
499 Function getter presets:
500    cimgui - Preset used for compiling libcimgui -> https://github.com/cimgui/cimgui
501 ";
502     helpInfo.options[3].help = "Don't execute Dpp, and don't generate the types file";
503     //Custom
504     helpInfo.options[4].help =r"
505 Flags m and g are always added, $1 must always match function without exports.
506 Examples: 
507     void func(char* str);
508     int main();
509 ";
510     //Prefix-only
511     helpInfo.options[5].help = r"
512 This will be the prefix of your regex.
513 The postfix will be a predefined one for function format:
514     Appends ^(?: at the start(The one which is meant to be ignored)
515     Appends )(.+\);)$ at the end (Finish the ignored one and append the function $1 one)
516 ";
517     //Plugin-load
518     helpInfo.options[6].help = r"
519 Loads plugins located at the plugins folder. For the plugin being loaded it must:
520     1: Export a function named export(Modulename) which returns a Plugin instance.
521     2: Have a compiled .dll or .so following the scheme 'libpluginPLUGIN_FOLDER_NAME'
522         2.1: If you need many exports in a single dll, create a package.d with public imports and
523         compile it, plugin finding is first folder only, i.e: not recursive.
524 ";
525     //Load all
526     helpInfo.options[7].help = r"
527 Loads every plugin located at the plugis folder";
528     //Plugins args
529     helpInfo.options[8].help = r"
530 Plugins arguments to pass into the entrance point.
531 Only the plugins with at least args 1 arg will be executed, pass a null string if you wish
532 to pass only the current working dir.
533 
534 Example on multiple args-> -a myplugin=[arg1 arg2 arg3]
535 
536 Reserved arguments are:
537     d-conv -> Converts from C to D
538 ";
539     //Recompile
540     helpInfo.options[9].help = r"
541 Using this option will force a recompilation of the plugins!";
542     //Debug
543     helpInfo.options[10].help = r"
544 Compile dynamic libraries with debug symbols enabled";
545 }
546 
547 bool checkHelpNeeded(ref GetoptResult helpInfo)
548 {
549     if(helpInfo.helpWanted || (optFile == "" && optUsingPlugins.length == 0))
550     {
551         if(optFile == "" && optUsingPlugins.length == 0)
552             writeln("File options is missing, you should always specify the target or specify a plugin!");
553         
554         defaultGetoptPrinter(r"
555 Bindbc-generator options.
556 If you find an issue with the content generation, report it at
557 https://www.github.com/MrcSnm/bindbc-generator
558 ",
559         helpInfo.options);
560         return true;
561     }
562     return false;
563 }
564 
565 bool checkPluginLoad()
566 {
567     if(optUsingPlugins.length != 0 || optLoadAll)
568     {
569         if(optLoadAll)
570             optUsingPlugins = PluginAdapter.loadPlugins(optUsingPlugins, optRecompile, optDebug);
571         else
572             PluginAdapter.loadPlugins(optUsingPlugins, optRecompile, optDebug);
573         int nullCount = 0;
574         if(optUsingPlugins.length == 0)
575         {
576             writeln("\n\nERROR!\nCould not load any plugin!");
577             return false;
578         }
579         foreach(p; optPluginArgs)
580         {
581             if(p.length == 0)
582                 nullCount++;
583         }
584         if(nullCount == optPluginArgs.length)
585         {
586             writeln(r"
587 Plugins loaded but none was specified for execution!
588 For executing it, you must at least specify one plugin arg.
589 Showing loaded plugins help info:");
590             foreach(k, v; PluginAdapter.loadedPlugins)
591             {
592                 if(v.getHelpInformation() == "")
593                     writeln("\n\nWARNING!\n\nContact ", k, " provider! No help information is given");
594                 else
595                     writeln("\n\n", k, "\n\n",
596 r"--------------------------------",
597 v.getHelpInformation());
598             }
599             return false;
600         }
601     }
602     return true;
603 }
604 
605 bool checkDppExecution()
606 {
607     string _f = optFile;
608     if(!optNoTypes)
609     {
610         if(optDppArgs == "")
611         {
612             writeln(r"
613 No dpp arg specified!
614 Beware that for this project uses dpp for generating struct and enums ONLY
615 Functions definitions comes from the .h file specified and then replaces with
616 D style
617 ");
618         }
619         File f = createDppFile(_f);
620         if(f.name == "")
621             return false;
622         executeDpp(f, optDppArgs);
623     }
624     return true;
625 }
626 
627 bool checkPluginOnly()
628 {
629     return (optFile == "" && optPresets == "" && optCustom == "" && optDppArgs == "" &&
630     (optUsingPlugins.length != 0 || optLoadAll));
631 }
632 
633 int main(string[] args)
634 {
635     GetoptResult helpInfo;
636     try
637     {
638         helpInfo = getopt(
639             args,
640             "dpparg|d", &optDppArgs,
641             "file|f", &optFile,
642             "presets|p", &optPresets,
643             "notypes|n", &optNoTypes,
644             "custom|c", &optCustom,
645             "use-func-prefix|u", &optFuncPrefix,
646             "load-plugins|l", &optUsingPlugins,
647             "load-all", &optLoadAll,
648             "plugin-args|a", &pluginArgsHandler,
649             "recompile|r", &optRecompile,
650             "debug", &optDebug
651         );
652     }
653     catch(Exception e)
654     {
655         writeln(e.msg);
656         return Plugin.ERROR;
657     }
658     helpInfoSetup(helpInfo);
659     //I don't really understand what type is regex...
660     Regex!char targetReg;
661 
662     if(!checkPluginLoad())
663         return Plugin.ERROR;
664     getDConvPlugins();
665 
666     bool pluginOnly = checkPluginOnly();
667 
668     if(!pluginOnly)
669     {
670         checkPresets(targetReg);
671         checkCustomRegex(targetReg);
672     }
673 
674     if(checkHelpNeeded(helpInfo))
675         return 1;
676     
677     if(!pluginOnly)
678     {
679         if(!checkDppExecution())
680             return Plugin.ERROR;
681         if(optPresets == "" && optCustom == "")
682         {
683             writeln("ERROR:\nNo regex nor presets for getting functions specified\n");
684             return Plugin.ERROR;
685         }
686         string funcs = getFuncs(optFile, targetReg);
687         if(funcs == "")
688         {
689             writeln("ERROR:\nNo hit was made by your function");
690             return Plugin.ERROR;
691         }
692         string cleanFuncs = cleanPreFuncsDeclaration(funcs, targetReg);
693         string dfuncs = cppFuncsToD(cleanFuncs);
694         string[] darrFuncs = dfuncs.split("\n");
695 
696         //It will already remove darrFuncs params
697         string libName = stripExtension(optFile);
698         createFuncsFile(libName, darrFuncs);
699         createLibLoad(libName, darrFuncs);    
700         createPackage(libName);
701 
702         if(!optNoTypes)
703             remove(optFile.stripExtension ~ ".d");
704     }
705     playPlugins(args[0]);
706     
707     return Plugin.SUCCESS;
708 }