1#####
2#
3# aspy.py
4#
5# Andy Hammerlindl 2011/09/03
6#
7# Uses ctypes to interface with the shared library version of Python.
8# Asymptote can run and its datatypes inspected via Python.
9#
10#
11# To use the module:
12# 1. make asymptote.so
13# 2. Ensure that asymptote.so is visable to python, say adding its directory
14#    to LD_LIBRARY_PATH
15# 3. Run this module.  (See runExample for an example.)
16#
17#####
18
19from ctypes import *
20
21asyInt = c_longlong
22handle_typ = c_void_p
23arguments_typ = c_void_p
24state_typ = c_void_p
25
26function_typ = CFUNCTYPE(None, state_typ, c_void_p)
27
28class string_typ(Structure):
29    _fields_ = [
30            ("buf", c_char_p), # Should be NUL-terminated? Maybe replace with
31                               # POINTER(c_char).
32            ("length", asyInt)
33            ]
34
35ErrorCallbackFUNC = CFUNCTYPE(None, string_typ)
36
37NORMAL_ARG = 45000
38REST_ARG = 45001
39
40class Policy(Structure):
41    _fields_ = [
42            ("version",            asyInt),
43            ("copyHandle",         CFUNCTYPE(handle_typ, handle_typ)),
44            ("releaseHandle",      CFUNCTYPE(None, handle_typ)),
45            ("handleFromInt",      CFUNCTYPE(handle_typ, asyInt)),
46            ("handleFromBool",      CFUNCTYPE(handle_typ, asyInt)),
47            ("handleFromDouble",      CFUNCTYPE(handle_typ, c_double)),
48            ("handleFromString",   CFUNCTYPE(handle_typ, string_typ)),
49            ("handleFromFunction", CFUNCTYPE(handle_typ,
50                                             c_char_p,
51                                             function_typ,
52                                             c_void_p)),
53            ("IntFromHandle",      CFUNCTYPE(asyInt, handle_typ)),
54            ("boolFromHandle",     CFUNCTYPE(asyInt, handle_typ)),
55            ("doubleFromHandle",   CFUNCTYPE(c_double, handle_typ)),
56            ("stringFromHandle",   CFUNCTYPE(string_typ, handle_typ)),
57            ("getField",           CFUNCTYPE(handle_typ,
58                                             handle_typ,
59                                             c_char_p)),
60            ("getCell",            CFUNCTYPE(handle_typ,
61                                             handle_typ,
62                                             asyInt)),
63            ("addField",           CFUNCTYPE(None,
64                                             handle_typ,
65                                             c_char_p,
66                                             handle_typ)),
67            ("newArguments",       CFUNCTYPE(arguments_typ)),
68            ("releaseArguments",   CFUNCTYPE(None, arguments_typ)),
69            ("addArgument",        CFUNCTYPE(None,
70                                             arguments_typ,
71                                             c_char_p,
72                                             handle_typ,
73                                             asyInt)),
74            ("call",               CFUNCTYPE(handle_typ,
75                                             handle_typ,
76                                             arguments_typ)),
77            ("globals",            CFUNCTYPE(handle_typ, state_typ)),
78            ("numParams",          CFUNCTYPE(asyInt, state_typ)),
79            ("getParam",           CFUNCTYPE(handle_typ, state_typ, asyInt)),
80            ("setReturnValue",     CFUNCTYPE(None, state_typ, handle_typ)),
81            ("setErrorCallback",   CFUNCTYPE(None, ErrorCallbackFUNC)),
82            ]
83
84policy = None
85baseState = None
86def initPolicyAndBaseState():
87    global policy, baseState
88    lib = CDLL("asymptote.so")
89
90    getPolicy = lib._asy_getPolicy
91    getPolicy.restype = POINTER(Policy)
92    policy = getPolicy()
93
94    getState = lib._asy_getState
95    getState.restype = state_typ
96    baseState = getState()
97
98initPolicyAndBaseState()
99
100def pyStringFromAsyString(st):
101    #TODO: Handle strings with null-terminators.
102    return str(st.buf)
103
104def pyStringFromHandle(h):
105    #TODO: Handle strings with null-terminators.
106    st = policy.contents.stringFromHandle(h)
107    checkForErrors()
108    return pyStringFromAsyString(st)
109
110def handleFromPyString(s):
111    st = string_typ(s, len(s))
112    h = policy.contents.handleFromString(st)
113    checkForErrors()
114    return h
115
116def ensureDatum(val):
117    return val if type(val) is Datum else Datum(val)
118
119# The error detection scheme.
120# policyError is set to a string when an error occurs.
121policyError = []
122def pyErrorCallback(s):
123    global policyError
124    policyError.append(pyStringFromAsyString(s))
125
126cErrorCallback = ErrorCallbackFUNC(pyErrorCallback)
127policy.contents.setErrorCallback(cErrorCallback)
128
129class AsyException(Exception):
130    def __init__(self, msg):
131        self.msg = msg
132    def __str__(self):
133        return self.msg
134
135def checkForErrors():
136    """Raises an exception if an error occured."""
137    global policyError
138    if policyError != []:
139        s = policyError[0]
140        if len(policyError) > 1:
141            s += ' (and other errors)'
142        policyError = []
143        raise AsyException(s)
144
145class Datum(object):
146
147    def _setHandle(self, handle):
148        object.__setattr__(self, 'handle', handle)
149
150    def __init__(self, val):
151        self._setHandle(0)
152
153        if val is None:
154            return
155
156        if type(val) is int:
157            self._setHandle(policy.contents.handleFromInt(val))
158            checkForErrors()
159        elif type(val) is bool:
160            self._setHandle(policy.contents.handleFromBool(1 if val else 0))
161            checkForErrors()
162        elif type(val) is float:
163            self._setHandle(policy.contents.handleFromDouble(val))
164        elif type(val) is str:
165            self._setHandle(handleFromPyString(val))
166            checkForErrors()
167        elif type(val) is tuple:
168            # Could do this more efficiently, and avoid a copyHandle
169            ret = state.globals()["operator tuple"](*val)
170            self._setHandle(policy.contents.copyHandle(ret.handle))
171            checkForErrors()
172        elif type(val) is Datum:
173            self._setHandle(policy.contents.copyHandle(val.handle))
174            checkForErrors()
175        else:
176            # TODO: check if val has a toDatum field
177            raise TypeError("cannot initialize Datum from '%s'" %
178                    type(val).__name__)
179
180    def __repr__(self):
181        # TODO: Add type-checking to policy.
182        return '<Datum with handle %s>' % hex(self.handle)
183
184    def __int__(self):
185        l = policy.contents.IntFromHandle(self.handle)
186        checkForErrors()
187        return int(l)
188
189    def __nonzero__(self):
190        # This will throw an exception for anything but an underlying bool
191        # type.  Perhaps we should be more pythonic.
192        l = policy.contents.boolFromHandle(self.handle)
193        checkForErrors()
194        assert l in [0,1]
195        return l == 1
196
197    def __float__(self):
198        x = policy.contents.doubleFromHandle(self.handle)
199        checkForErrors()
200        return float(x)
201
202    def __str__(self):
203        return pyStringFromHandle(self.handle)
204
205    def __getattr__(self, name):
206        field = policy.contents.getField(self.handle, name)
207        checkForErrors()
208        return DatumFromHandle(field)
209
210    def __getitem__(self, name):
211        assert type(name) == str
212        return self.__getattr__(name)
213        #TODO: raise an IndexError when appropriate.
214        #TODO: implement array indices
215
216    def __setattr__(self, name, val):
217        # TODO: Resolve setting versus declaring.
218        # One idea: d.x = f or d["x"] = f sets and d["int x()"] = f declares
219        # anew.
220        policy.contents.addField(self.handle,
221                name, ensureDatum(val).handle)
222        checkForErrors()
223
224    def __setitem__(self, name, val):
225        assert type(name) == str
226        self.__setattr__(name, val)
227        #TODO: raise an IndexError when appropriate.
228        #TODO: implement array indices
229
230    def __call__(self, *args, **namedArgs):
231        alist = policy.contents.newArguments()
232        checkForErrors()
233
234
235        for arg in args:
236            d = ensureDatum(arg)
237            policy.contents.addArgument(alist, "", d.handle, NORMAL_ARG)
238            checkForErrors()
239
240        for name,arg in namedArgs.items():
241            d = ensureDatum(arg)
242            policy.contents.addArgument(alist, name, d.handle, NORMAL_ARG)
243            checkForErrors()
244
245        ret = policy.contents.call(self.handle, alist)
246        checkForErrors()
247
248        policy.contents.releaseArguments(alist)
249        checkForErrors()
250
251        if ret != None:
252            return DatumFromHandle(ret)
253
254    def __add__(self, other):
255        return state.globals()["operator +"](self, other)
256    def __sub__(self, other):
257        return state.globals()["operator -"](self, other)
258    def __mul__(self, other):
259        return state.globals()["operator *"](self, other)
260    def __div__(self, other):
261        return state.globals()["operator /"](self, other)
262    def __truediv__(self, other):
263        return state.globals()["operator /"](self, other)
264    def __mod__(self, other):
265        return state.globals()["operator %"](self, other)
266    def __pow__(self, other):
267        return state.globals()["operator ^"](self, other)
268    def __and__(self, other):
269        return state.globals()["operator &"](self, other)
270    def __or__(self, other):
271        return state.globals()["operator |"](self, other)
272    def __neg__(self, other):
273        return state.globals()["operator -"](self)
274
275    def __lt__(self, other):
276        return state.globals()["operator <"](self, other)
277    def __le__(self, other):
278        return state.globals()["operator <="](self, other)
279    def __eq__(self, other):
280        return state.globals()["operator =="](self, other)
281    def __ne__(self, other):
282        return state.globals()["operator !="](self, other)
283    def __gt__(self, other):
284        return state.globals()["operator >"](self, other)
285    def __ge__(self, other):
286        return state.globals()["operator >="](self, other)
287
288def DatumFromHandle(handle):
289    """Initializes a Datum from a given low-level handle.  Does not invoke
290    copyHandle."""
291    d = Datum(None)
292    d._setHandle(handle)
293    return d
294
295class State(object):
296    def __init__(self, base):
297        self.base = base
298
299    def globals(self):
300        handle = policy.contents.globals(self.base)
301        checkForErrors()
302        return DatumFromHandle(handle)
303
304    def params(self):
305        p = []
306
307        numParams = policy.contents.numParams(self.base)
308        checkForErrors()
309
310        for i in range(numParams):
311            h = policy.contents.getParam(self.base, i)
312            checkForErrors()
313            p.append(DatumFromHandle(h))
314
315        assert len(p) == numParams
316        return p
317
318    def setReturnValue(self, val):
319        policy.contents.setReturnValue(self.base, ensureDatum(val).handle)
320        checkForErrors()
321
322# Keep a link to all of the callbacks created, so they aren't garbage
323# collected.  TODO: See if this is neccessary.
324storedCallbacks = []
325
326def DatumFromCallable(f):
327    def wrapped(s, d):
328        state = State(s)
329        params = state.params()
330        r = f(*params)
331        if r != None:
332            state.setReturnValue(r)
333
334    cf = function_typ(wrapped)
335    storedCallbacks.append(cf)
336
337    h = policy.contents.handleFromFunction(f.__name__, cf, None)
338    checkForErrors()
339
340    return DatumFromHandle(h)
341
342print "version", policy.contents.version
343
344state = State(baseState)
345
346# An example
347def runExample():
348    g = state.globals()
349
350    g.eval("path p = (0,0) -- (100,100) -- (200,0)", embedded=True)
351    g.draw(g.p)
352    g.shipout("frompython")
353
354    g.draw(g.circle(100), g.red)
355
356