1 /*
2 Copyright (c) 2012 Ellery Newcomer
3 
4 Permission is hereby granted, free of charge, to any person obtaining a copy of
5 this software and associated documentation files (the "Software"), to deal in
6 the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 of the Software, and to permit persons to whom the Software is furnished to do
9 so, subject to the following conditions:
10 
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13 
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 */
22 
23 /// Contains utilities for embedding python in D.
24 ///
25 /// Importing this module will call Py_Initialize.
26 module pyd.embedded;
27 
28 /+
29  + Things we want to do:
30  +  * run python code in D (easily) [check]
31  +  * run D code in python [check]
32  +  * declare python functions and use them in D [check]
33  +  * access and manipulate python globals in D [check]
34  +  * wrap D classes/structs and use them in python or D [check]
35  +  * use python class instances in D [check]
36  +  * wrap D ranges and iterators or whatever and iterate through them in python [why?]
37  +  * wrap python iterators as D input ranges [check]
38  +  * do things with inheritance [why??!??]
39  +  * do things with multithreading
40  +/
41 
42 import deimos.python.Python;
43 import pyd.pyd;
44 import util.conv;
45 import std.algorithm: findSplit;
46 import std.string: strip, outdent;
47 import std.traits;
48 
49 /++
50  + Fetch a python module object.
51  +/
52 PydObject py_import(string name) {
53     debug assert(Py_IsInitialized(), "python not initialized");
54     return new PydObject(PyImport_ImportModule(zcc(name)));
55 }
56 
57 /**
58   Encapsulate a context within the Python interpreter.
59 
60   This will preserve local variables and changes to the Python interpreter
61   made by 'from ___future__ import feature' across calls to this.py_eval and
62   this.py_stmts.
63 
64   Otherwise, will not segregate global changes made to the Python interpreter.
65   */
66 class InterpContext {
67     /// dict object: global variables in this context
68     PydObject globals;
69     /// dict object: local variables in this context
70     PydObject locals;
71     PyCompilerFlags flags;
72     PyFrameObject* frame;
73 
74     /**
75       */
76     this() {
77         debug assert(Py_IsInitialized(), "python not initialized");
78         locals = new PydObject(PyDict_New());
79         globals = py(["__builtins__": new PydObject(PyEval_GetBuiltins())]);
80     }
81 
82     void pushDummyFrame() {
83         auto threadstate = PyThreadState_GET();
84         if(threadstate.frame == null) {
85             PyCodeObject* code = PyCode_NewEmpty("<d>","<d>", 0);
86             frame = PyFrame_New(threadstate, code,
87                     cast(PyObject*)(globals.ptr),
88                     cast(PyObject*)(locals.ptr));
89             threadstate.frame = frame;
90         }
91     }
92 
93     void popDummyFrame() {
94         auto threadstate = PyThreadState_GET();
95         if(threadstate.frame == frame) {
96             threadstate.frame = null;
97         }
98     }
99 
100     /**
101       Evaluate a python expression once within this context and return the
102       result.
103 
104 Params:
105 python = a python expression
106 Returns:
107 the result of expression
108      */
109     T py_eval(T = PydObject)(
110             string python,
111             string file = __FILE__,
112             size_t line = __LINE__) {
113 
114         auto pres = PyRun_StringFlags(
115                 zcc(python),
116                 Py_eval_input,
117                 cast(PyObject*) globals.ptr,
118                 cast(PyObject*) locals.ptr,
119                 &flags);
120         if(pres) {
121             auto res = new PydObject(pres);
122             return res.to_d!T();
123         }else{
124             handle_exception(file,line);
125             assert(0);
126         }
127     }
128     /**
129       Evaluate one or more python statements once.
130 
131 Params:
132 python = python statements
133      */
134     void py_stmts(string python,
135             string file = __FILE__,
136             size_t line = __LINE__) {
137         import std.string: outdent;
138 
139         auto pres = PyRun_StringFlags(
140                 zcc(outdent(python)),
141                 Py_file_input,
142                 cast(PyObject*) globals.ptr,
143                 cast(PyObject*) locals.ptr,
144                 &flags);
145         if(pres) {
146             Py_DECREF(pres);
147         }else{
148             handle_exception(file,line);
149         }
150     }
151 
152     @property PydObject opDispatch(string id)() {
153         return this.locals[id];
154     }
155 
156     @property void opDispatch(string id, T)(T t) {
157         static if(is(T == PydObject)) {
158             alias t s;
159         }else{
160             PydObject s = py(t);
161         }
162         this.locals[id] = py(s);
163     }
164 }
165 
166 /++
167 Wraps a python function (specified as a string) as a D function roughly of
168 signature func_t
169 
170 Params:
171 python = a python function
172 modl = context in which to run expression. must be a python module name.
173 func_t = type of d function
174  +/
175 ReturnType!func_t py_def( string python, string modl, func_t)
176     (ParameterTypeTuple!func_t args,
177      string file = __FILE__, size_t line = __LINE__) {
178     //Note that type is really the only thing that need be static here, but hey.
179         alias ReturnType!func_t R;
180         alias ParameterTypeTuple!func_t Args;
181     enum afterdef = findSplit(python, "def")[2];
182     enum ereparen = findSplit(afterdef, "(")[0];
183     enum name = strip(ereparen) ~ "\0";
184     static PydObject func;
185     static PythonException exc;
186     static string errmsg;
187     static bool once = true;
188     debug assert(Py_IsInitialized(), "python not initialized");
189     if(once) {
190         once = false;
191         auto globals = py_import(modl).getdict();
192         auto globals_ptr = Py_INCREF(globals.ptr);
193         scope(exit) Py_DECREF(globals_ptr);
194         auto locals = py((string[string]).init);
195         auto locals_ptr = Py_INCREF(locals.ptr);
196         scope(exit) Py_DECREF(locals_ptr);
197         if("__builtins__" !in globals) {
198             auto builtins = new PydObject(PyEval_GetBuiltins());
199             globals["__builtins__"] = builtins;
200         }
201         auto pres = PyRun_String(
202                     zcc(python),
203                     Py_file_input, globals_ptr, locals_ptr);
204         if(pres) {
205             auto res = new PydObject(pres);
206             func = locals[name];
207         }else{
208             try{
209                 handle_exception();
210             }catch(PythonException e) {
211                 exc = e;
212             }
213         }
214     }
215     if(!func) {
216         throw exc;
217     }
218     return func(args).to_d!R();
219 }
220 
221 /++
222 Evaluate a python expression once and return the result.
223 
224 Params:
225 python = a python expression
226 modl = context in which to run expression. either a python module name, or "".
227  +/
228 T py_eval(T = PydObject)(string python, string modl = "", string file = __FILE__, size_t line = __LINE__) {
229     debug assert(Py_IsInitialized(), "python not initialized");
230     PydObject locals = null;
231     PyObject* locals_ptr = null;
232     if(modl == "") {
233         locals = py((string[string]).init);
234     }else {
235         locals = py_import(modl).getdict();
236     }
237         locals_ptr = Py_INCREF(locals.ptr);
238     if("__builtins__" !in locals) {
239         auto builtins = new PydObject(PyEval_GetBuiltins());
240         locals["__builtins__"] = builtins;
241     }
242     auto pres = PyRun_String(
243             zcc(python),
244             Py_eval_input, locals_ptr, locals_ptr);
245     scope(exit) Py_XDECREF(locals_ptr);
246     if(pres) {
247         auto res = new PydObject(pres);
248         return res.to_d!T();
249     }else{
250         handle_exception(file,line);
251         assert(0);
252     }
253 }
254 
255 /++
256 Evaluate one or more python statements once.
257 
258 Params:
259 python = python statements
260 modl = context in which to run expression. either a python module name, or "".
261  +/
262 void py_stmts(string python, string modl = "",string file = __FILE__, size_t line = __LINE__) {
263     debug assert(Py_IsInitialized(), "python not initialized");
264     PydObject locals;
265     PyObject* locals_ptr;
266     if(modl == "") {
267         locals = py((string[string]).init);
268     }else {
269         locals = py_import(modl).getdict();
270     }
271     locals_ptr = Py_INCREF(locals.ptr);
272     if("__builtins__" !in locals) {
273         auto builtins = new PydObject(PyEval_GetBuiltins());
274         locals["__builtins__"] = builtins;
275     }
276     auto pres = PyRun_String(
277             zcc(outdent(python)),
278             Py_file_input, locals_ptr, locals_ptr);
279     scope(exit) Py_DECREF(locals_ptr);
280     if(pres) {
281         Py_DECREF(pres);
282     }else{
283         handle_exception(file,line);
284     }
285 }
286