1 /**
2    Wrapping functionality for D to Python.
3  */
4 module autowrap.pynih.wrap;
5 
6 
7 public import autowrap.types: Modules, Module, isModule,
8     LibraryName, PreModuleInitCode, PostModuleInitCode, RootNamespace, Ignore;
9 public import std.typecons : Yes, No;
10 
11 import python.raw: PyDateTime_CAPI, PyObject;
12 static import python.boilerplate;
13 import autowrap.common: toSnakeCase;
14 import mirror.meta.reflection : FunctionSymbol;
15 import std.meta: allSatisfy;
16 import std.traits: isInstanceOf, isSomeFunction;
17 
18 // This is required to avoid linker errors. Before it was in the string mixin,
19 // but there's no need for it there, instead we declare it here in the library
20 // instead.
21 export __gshared extern(C) PyDateTime_CAPI* PyDateTimeAPI;
22 
23 
24 /**
25    Returns a string to mixin that implements the necessary boilerplate
26    to create a Python library containing one Python module
27    wrapping all relevant D code and data structures.
28  */
29 string wrapDlang(
30     LibraryName libraryName,
31     Modules modules,
32     RootNamespace _ = RootNamespace(), // ignored in this backend
33     PreModuleInitCode preModuleInitCode = PreModuleInitCode(),    // ignored in this backend
34     PostModuleInitCode postModuleInitCode = PostModuleInitCode(), // ignored in this backend
35     )
36     ()
37 {
38     assert(__ctfe);
39     return createPythonModuleMixin!(libraryName, modules);
40 }
41 
42 
43 /**
44    Returns a string with the module creation function for Python, i.e.
45    the Python extension module's entry point. Needs to be a string mixin
46    since Python knows which function is the entry point by name convention,
47    where an extension module called "foo" needs to export a function
48    `PyInit_foo`. "All" this function does is create a function with the
49    appropriate name and stringify the arguments to pass to the function
50    doing the heavy lifting: createPythonModule.
51  */
52 string createPythonModuleMixin(LibraryName libraryName, Modules modules)
53                               ()
54 {
55     import std.format: format;
56     import std.algorithm: map;
57     import std.array: join;
58 
59     assert(__ctfe);
60 
61     return q{
62         extern(C) export auto PyInit_%s() { // -> ModuleInitRet
63             import autowrap.pynih.wrap: createPythonModule, LibraryName;
64             import autowrap.types: Module;
65             return createPythonModule!(
66                 LibraryName("%s"),
67                 %s
68             );
69         }
70     }.format(
71         libraryName.value,
72         libraryName.value,
73         modules.value.map!(a => a.toString).join(", ")
74     );
75 }
76 
77 
78 /**
79    Reflects on the modules given and creates a Python module called `libraryName`
80    with wrappers for all of the functions and types in each of the modules.
81  */
82 auto createPythonModule(LibraryName libraryName, modules...)()
83     if(allSatisfy!(isModule, modules))
84 {
85     import python.boilerplate: Module;
86 
87     auto ret = createDlangPythonModule!(
88         Module(libraryName.value),
89         cfunctions!modules,
90         aggregates!modules
91     );
92 
93     addConstants!modules(ret);
94 
95     return ret;
96 }
97 
98 void addConstants(modules...)(PyObject* pythonModule) {
99     import python.cooked : addStringConstant, addIntConstant;
100     import autowrap.reflection: AllConstants;
101     import std.meta: Filter;
102 
103     alias constants = AllConstants!modules;
104 
105     template isIntegral(alias var) {
106         import std.traits: _isIntegral = isIntegral;
107         enum isIntegral = _isIntegral!(var.Type);
108     }
109     static foreach(intConstant; Filter!(isIntegral, constants)) {
110         addIntConstant!(intConstant.identifier, intConstant.value)(pythonModule);
111     }
112 
113     enum isString(alias var) = is(var.Type == string);
114     static foreach(strConstant; Filter!(isString, constants)) {
115         addStringConstant!(strConstant.identifier, strConstant.value)(pythonModule);
116     }
117 }
118 
119 /**
120    The C functions that Python is actually going to call, of the type
121    PyObject* (PyObject* args, PyObject* kwargs). "Returned" as `python.boilerplate.CFunctions`,
122    obtained by reflecting on the passed-in modules and synthethising the necessary functions
123    by doing all conversions automatically.
124  */
125 template cfunctions(modules...) {
126     import python.boilerplate: CFunctions;
127     import std.meta: staticMap;
128     alias cfunctions = CFunctions!(staticMap!(toCFunction, wrappableFunctions!modules));
129 }
130 
131 private template wrappableFunctions(modules...) {
132     import autowrap.common: AlwaysTry;
133     import python.type: PythonFunction;
134     import autowrap.reflection: AllFunctions;
135     import std.meta: Filter, templateNot;
136     import std.traits: fullyQualifiedName;
137 
138     alias allFunctions = AllFunctions!modules;
139     enum isWrappableFunction(alias functionSymbol) = AlwaysTry ||
140         __traits(compiles, &PythonFunction!(functionSymbol.symbol)._py_function_impl);
141     alias nonWrappableFunctions = Filter!(templateNot!isWrappableFunction, allFunctions);
142 
143     static foreach(nonWrappableFunction; nonWrappableFunctions) {
144             pragma(msg, "autowrap WARNING: Could not wrap function ",
145                    fullyQualifiedName!(nonWrappableFunction.symbol));
146         // uncomment to see the compiler error
147         // &PythonFunction!(nonWrappableFunction.symbol)._py_function_impl;
148     }
149 
150     alias wrappableFunctions = Filter!(isWrappableFunction, allFunctions);
151 }
152 
153 /**
154    The D aggregates (structs/classes/enums) to be wrapped for Python.
155    "Returned" as `python.boilerplate.Aggregates`
156  */
157 template aggregates(modules...) {
158     import python.boilerplate: Aggregates;
159     import autowrap.reflection: AllAggregates;
160     alias aggregates = Aggregates!(AllAggregates!modules);
161 }
162 
163 /**
164    Converts from mirror.meta.reflection.FunctionSymbol to the template CFunction from python.boilerplate.
165    The reason identifier defaults to an empty string is because otherwise it doesn't compile
166    if a regular D function symbol is passed.
167  */
168 template toCFunction(alias functionSymbol, string identifier = "")
169     if(isInstanceOf!(FunctionSymbol, functionSymbol))
170 {
171     import python.type: PythonFunction;
172     import python.boilerplate: CFunction;
173 
174     private enum id = identifier == "" ? functionSymbol.identifier.toSnakeCase : identifier;
175 
176     alias toCFunction = CFunction!(
177         PythonFunction!(functionSymbol.symbol)._py_function_impl,
178         id,
179     );
180 }
181 
182 template toCFunction(alias F, string identifier = __traits(identifier, F).toSnakeCase)
183     if(isSomeFunction!F)
184 {
185     import python.type: PythonFunction;
186     import python.boilerplate: CFunction;
187 
188     alias toCFunction = CFunction!(
189         PythonFunction!F._py_function_impl,
190         identifier,
191     );
192 }
193 
194 /**
195    Initialises the Python DateTime API, the druntime, and creates the Python extension module
196  */
197 auto createDlangPythonModule(python.boilerplate.Module module_, alias cfunctions, alias aggregates)() {
198     import python.raw: pyDateTimeImport;
199     import python.cooked: createModule;
200     import core.runtime: rt_init;
201 
202     rt_init;
203     pyDateTimeImport;
204 
205     return createModule!(module_, cfunctions, aggregates);
206 }