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