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 pyd.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 file =
107 line =
108 Returns:
109 the result of expression
110      */
111     T py_eval(T = PydObject)(
112             string python,
113             string file = __FILE__,
114             size_t line = __LINE__) {
115 
116         auto pres = PyRun_StringFlags(
117                 zcc(python),
118                 Py_eval_input,
119                 cast(PyObject*) globals.ptr,
120                 cast(PyObject*) locals.ptr,
121                 &flags);
122         if(pres) {
123             auto res = new PydObject(pres);
124             return res.to_d!T();
125         }else{
126             handle_exception(file,line);
127             assert(0);
128         }
129     }
130     /**
131       Evaluate one or more python statements once.
132 
133     Params:
134      python =
135      python = python statements
136      file = 
137      line =
138      */
139     void py_stmts(string python,
140             string file = __FILE__,
141             size_t line = __LINE__) {
142         import std.string: outdent;
143 
144         auto pres = PyRun_StringFlags(
145                 zcc(outdent(python)),
146                 Py_file_input,
147                 cast(PyObject*) globals.ptr,
148                 cast(PyObject*) locals.ptr,
149                 &flags);
150         if(pres) {
151             Py_DECREF(pres);
152         }else{
153             handle_exception(file,line);
154         }
155     }
156 
157     @property PydObject opDispatch(string id)() {
158         return this.locals[id];
159     }
160 
161     @property void opDispatch(string id, T)(T t) {
162         static if(is(T == PydObject)) {
163             alias t s;
164         }else{
165             PydObject s = py(t);
166         }
167         this.locals[id] = py(s);
168     }
169 }
170 
171 /++
172 Wraps a python function (specified as a string) as a D function roughly of
173 signature func_t
174 
175 Template Parameters:
176 python = a python function
177 modl = context in which to run expression. must be a python module name.
178 func_t = type of d function
179  +/
180 ReturnType!func_t py_def( string python, string modl, func_t)
181     (ParameterTypeTuple!func_t args,
182      string file = __FILE__, size_t line = __LINE__) {
183     //Note that type is really the only thing that need be static here, but hey.
184         alias ReturnType!func_t R;
185         alias ParameterTypeTuple!func_t Args;
186     enum afterdef = findSplit(python, "def")[2];
187     enum ereparen = findSplit(afterdef, "(")[0];
188     enum name = strip(ereparen) ~ "\0";
189     static PydObject func;
190     static PythonException exc;
191     static string errmsg;
192     static bool once = true;
193     debug assert(Py_IsInitialized(), "python not initialized");
194     if(once) {
195         once = false;
196         auto globals = py_import(modl).getdict();
197         auto globals_ptr = Py_INCREF(globals.ptr);
198         scope(exit) Py_DECREF(globals_ptr);
199         auto locals = py((string[string]).init);
200         auto locals_ptr = Py_INCREF(locals.ptr);
201         scope(exit) Py_DECREF(locals_ptr);
202         if("__builtins__" !in globals) {
203             auto builtins = new PydObject(PyEval_GetBuiltins());
204             globals["__builtins__"] = builtins;
205         }
206         auto pres = PyRun_String(
207                     zcc(python),
208                     Py_file_input, globals_ptr, locals_ptr);
209         if(pres) {
210             auto res = new PydObject(pres);
211             func = locals[name];
212         }else{
213             try{
214                 handle_exception();
215             }catch(PythonException e) {
216                 exc = e;
217             }
218         }
219     }
220     if(!func) {
221         throw exc;
222     }
223     return func(args).to_d!R();
224 }
225 
226 /++
227 Evaluate a python expression once and return the result.
228 
229 Params:
230 python =
231 python = a python expression
232 modl = context in which to run expression. either a python module name, or "".
233 file =
234 line =
235  +/
236 T py_eval(T = PydObject)(string python, string modl = "", string file = __FILE__, size_t line = __LINE__) {
237     debug assert(Py_IsInitialized(), "python not initialized");
238     PydObject locals = null;
239     PyObject* locals_ptr = null;
240     if(modl == "") {
241         locals = py((string[string]).init);
242     }else {
243         locals = py_import(modl).getdict();
244     }
245         locals_ptr = Py_INCREF(locals.ptr);
246     if("__builtins__" !in locals) {
247         auto builtins = new PydObject(PyEval_GetBuiltins());
248         locals["__builtins__"] = builtins;
249     }
250     auto pres = PyRun_String(
251             zcc(python),
252             Py_eval_input, locals_ptr, locals_ptr);
253     scope(exit) Py_XDECREF(locals_ptr);
254     if(pres) {
255         auto res = new PydObject(pres);
256         return res.to_d!T();
257     }else{
258         handle_exception(file,line);
259         assert(0);
260     }
261 }
262 
263 /++
264 Evaluate one or more python statements once.
265 
266 Params:
267 python =
268 python = python statements
269 modl = context in which to run expression. either a python module name, or "".
270 file =
271 line =
272  +/
273 void py_stmts(string python, string modl = "",string file = __FILE__, size_t line = __LINE__) {
274     debug assert(Py_IsInitialized(), "python not initialized");
275     PydObject locals;
276     PyObject* locals_ptr;
277     if(modl == "") {
278         locals = py((string[string]).init);
279     }else {
280         locals = py_import(modl).getdict();
281     }
282     locals_ptr = Py_INCREF(locals.ptr);
283     if("__builtins__" !in locals) {
284         auto builtins = new PydObject(PyEval_GetBuiltins());
285         locals["__builtins__"] = builtins;
286     }
287     auto pres = PyRun_String(
288             zcc(outdent(python)),
289             Py_file_input, locals_ptr, locals_ptr);
290     scope(exit) Py_DECREF(locals_ptr);
291     if(pres) {
292         Py_DECREF(pres);
293     }else{
294         handle_exception(file,line);
295     }
296 }
297