source: framspy/frams.py @ 1182

Last change on this file since 1182 was 1178, checked in by Maciej Komosinski, 2 years ago

Windows and python 3.9.5+ compatibility: endless nuances/bugs in dll access

File size: 14.0 KB
Line 
1"""Framsticks as a Python module.
2
3Static FramScript objects are available inside the module under their well known names
4(frams.Simulator, frams.GenePools, etc.)
5
6These objects and all values passed to and from Framsticks are instances of frams.ExtValue.
7Python values are automatically converted to Framstics data types.
8Use frams.ExtValue._makeInt()/_makeDouble()/_makeString()/_makeNull() for explicit conversions.
9Simple values returned from Framsticks can be converted to their natural Python
10counterparts using _value() (or forced to a specific type with  _int()/_double()/_string()).
11
12All non-Framsticks Python attributes start with '_' to avoid conflicts with Framsticks attributes.
13Framsticks names that are Python reserved words are prefixed with 'x' (currently just Simulator.ximport).
14
15For sample usage, see frams-test.py and FramsticksLib.py.
16
17If you want to run many independent instances of this class in parallel, use the "multiprocessing" module and then each process
18that uses this module will initialize it and get access to a separate instance of the Framsticks library.
19
20For interfaces in other languages (e.g. using the Framsticks library in your C++ code), see ../cpp/frams/frams-objects.h
21"""
22
23import ctypes, re, sys, os
24
25c_api = None  # will be initialized in init(). Global because ExtValue uses it.
26
27
28class ExtValue(object):
29        """All Framsticks objects and values are instances of this class. Read the documentation of the 'frams' module for more information."""
30
31        _reInsideParens = re.compile('\((.*)\)')
32        _reservedWords = ['import']  # this list is scanned during every attribute access, only add what is really clashing with Framsticks properties
33        _reservedXWords = ['x' + word for word in _reservedWords]
34        _encoding = 'utf-8'
35
36
37        def __init__(self, arg=None, dontinit=False):
38                if dontinit:
39                        return
40                if isinstance(arg, int):
41                        self._initFromInt(arg)
42                elif isinstance(arg, str):
43                        self._initFromString(arg)
44                elif isinstance(arg, float):
45                        self._initFromDouble(arg)
46                elif arg == None:
47                        self._initFromNull()
48                else:
49                        raise ctypes.ArgumentError("Can't make ExtValue from '%s' (%s)" % (str(arg), type(arg)))
50
51
52        def __del__(self):
53                c_api.extFree(self.__ptr)
54
55
56        def _initFromNull(self):
57                self.__ptr = c_api.extFromNull()
58
59
60        def _initFromInt(self, v):
61                self.__ptr = c_api.extFromInt(v)
62
63
64        def _initFromDouble(self, v):
65                self.__ptr = c_api.extFromDouble(v)
66
67
68        def _initFromString(self, v):
69                self.__ptr = c_api.extFromString(ExtValue._cstringFromPython(v))
70
71
72        @classmethod
73        def _makeNull(cls, v):
74                e = ExtValue(None, True)
75                e._initFromNull()
76                return e
77
78
79        @classmethod
80        def _makeInt(cls, v):
81                e = ExtValue(None, True)
82                e._initFromInt(v)
83                return e
84
85
86        @classmethod
87        def _makeDouble(cls, v):
88                e = ExtValue(None, True)
89                e._initFromDouble(v)
90                return e
91
92
93        @classmethod
94        def _makeString(cls, v):
95                e = ExtValue(None, True)
96                e._initFromString(v)
97                return e
98
99
100        @classmethod
101        def _rootObject(cls):
102                e = ExtValue(None, True)
103                e.__ptr = c_api.rootObject()
104                return e
105
106
107        @classmethod
108        def _stringFromC(cls, cptr):
109                return cptr.decode(ExtValue._encoding)
110
111
112        @classmethod
113        def _cstringFromPython(cls, s):
114                return ctypes.c_char_p(s.encode(ExtValue._encoding))
115
116
117        def _type(self):
118                return c_api.extType(self.__ptr)
119
120
121        def _class(self):
122                cls = c_api.extClass(self.__ptr)
123                if cls == None:
124                        return None
125                else:
126                        return ExtValue._stringFromC(cls)
127
128
129        def _value(self):
130                t = self._type()
131                if t == 0:
132                        return None
133                elif t == 1:
134                        return self._int()
135                elif t == 2:
136                        return self._double()
137                elif t == 3:
138                        return self._string()
139                else:
140                        return self
141
142
143        def _int(self):
144                return c_api.extIntValue(self.__ptr)
145
146
147        def _double(self):
148                return c_api.extDoubleValue(self.__ptr)
149
150
151        def _string(self):
152                return ExtValue._stringFromC(c_api.extStringValue(self.__ptr))
153
154
155        def _propCount(self):
156                return c_api.extPropCount(self.__ptr)
157
158
159        def _propId(self, i):
160                return ExtValue._stringFromC(c_api.extPropId(self.__ptr, i))
161
162
163        def _propName(self, i):
164                return ExtValue._stringFromC(c_api.extPropName(self.__ptr, i))
165
166
167        def _propType(self, i):
168                return ExtValue._stringFromC(c_api.extPropType(self.__ptr, i))
169
170
171        def _propHelp(self, i):
172                h = c_api.extPropHelp(self.__ptr, i)  # unlike other string fields, help is sometimes NULL
173                return ExtValue._stringFromC(h) if h != None else '';
174
175
176        def _propFlags(self, i):
177                return c_api.extPropFlags(self.__ptr, i)
178
179
180        def _propGroup(self, i):
181                return c_api.extPropGroup(self.__ptr, i)
182
183
184        def _groupCount(self):
185                return c_api.extGroupCount(self.__ptr)
186
187
188        def _groupName(self, i):
189                return ExtValue._stringFromC(c_api.extGroupName(self.__ptr, i))
190
191
192        def _groupMember(self, g, i):
193                return c_api.extGroupMember(self.__ptr, g, i)
194
195
196        def _memberCount(self, g):
197                return c_api.extMemberCount(self.__ptr, g)
198
199
200        def __str__(self):
201                return self._string()
202
203
204        def __dir__(self):
205                ids = dir(type(self))
206                if self._type() == 4:
207                        for i in range(c_api.extPropCount(self.__ptr)):
208                                name = ExtValue._stringFromC(c_api.extPropId(self.__ptr, i))
209                                if name in ExtValue._reservedWords:
210                                        name = 'x' + name
211                                ids.append(name)
212                return ids
213
214
215        def __getattr__(self, key):
216                if key[0] == '_':
217                        return self.__dict__[key]
218                if key in ExtValue._reservedXWords:
219                        key = key[1:]
220                prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
221                if prop_i < 0:
222                        raise AttributeError('no ' + str(key) + ' in ' + str(self))
223                t = ExtValue._stringFromC(c_api.extPropType(self.__ptr, prop_i))
224                if t[0] == 'p':
225                        arg_types = ExtValue._reInsideParens.search(t)
226                        if arg_types:
227                                arg_types = arg_types.group(1).split(',')  # anyone wants to add argument type validation using param type declarations?
228
229
230                        def fun(*args):
231                                ext_args = []
232                                ext_pointers = []
233                                for a in args:
234                                        if isinstance(a, ExtValue):
235                                                ext = a
236                                        else:
237                                                ext = ExtValue(a)
238                                        ext_args.append(ext)
239                                        ext_pointers.append(ext.__ptr)
240                                ret = ExtValue(None, True)
241                                args_array = (ctypes.c_void_p * len(args))(*ext_pointers)
242                                ret.__ptr = c_api.extPropCall(self.__ptr, prop_i, len(args), args_array)
243                                return ret
244
245
246                        return fun
247                else:
248                        ret = ExtValue(None, True)
249                        ret.__ptr = c_api.extPropGet(self.__ptr, prop_i)
250                        return ret
251
252
253        def __setattr__(self, key, value):
254                if key[0] == '_':
255                        self.__dict__[key] = value
256                else:
257                        if key in ExtValue._reservedXWords:
258                                key = key[1:]
259                        prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
260                        if prop_i < 0:
261                                raise AttributeError("No '" + str(key) + "' in '" + str(self) + "'")
262                        if not isinstance(value, ExtValue):
263                                value = ExtValue(value)
264                        c_api.extPropSet(self.__ptr, prop_i, value.__ptr)
265
266
267        def __getitem__(self, key):
268                return self.get(key)
269
270
271        def __setitem__(self, key, value):
272                return self.set(key, value)
273
274
275        def __len__(self):
276                try:
277                        return self.size._int()
278                except:
279                        return 0
280
281
282        def __iter__(self):
283                class It(object):
284                        def __init__(self, container, frams_it):
285                                self.container = container
286                                self.frams_it = frams_it
287
288
289                        def __iter__(self):
290                                return self
291
292
293                        def __next__(self):
294                                if self.frams_it.next._int() != 0:
295                                        return self.frams_it.value
296                                else:
297                                        raise StopIteration()
298
299                return It(self, self.iterator)
300
301
302def init(*args):
303        """
304        Initializes the connection to Framsticks dll/so/dylib.
305
306        Python programs do not have to know the Framstics path but if they know, just pass the path as the first argument.
307        Similarly '-dPATH' and '-DPATH' needed by Framsticks are optional and derived from the first path, unless they are specified as args in init().
308        '-LNAME' is the optional library name (full name including the file name extension), default is 'frams-objects.dll/.so' depending on the platform.
309        All other arguments are passed to Framsticks and not interpreted by this function.
310
311        """
312        # goals:
313        frams_d = None
314        frams_D = None
315        lib_path = None
316        lib_name = ('frams-objects.dylib' if sys.platform == 'darwin' else 'frams-objects.so') if os.name == 'posix' else 'frams-objects.dll'
317        initargs = []
318        for a in args:
319                if a[:2] == '-d':
320                        frams_d = a
321                elif a[:2] == '-D':
322                        frams_D = a
323                elif a[:2] == '-L':
324                        lib_name = a[2:]
325                elif lib_path is None:
326                        lib_path = a
327                else:
328                        initargs.append(a)
329        if lib_path is None:
330                # TODO: use environment variable and/or the zip distribution we are in when the path is not specified in arg
331                # for now just assume the current dir is Framsticks
332                lib_path = '.'
333
334        if os.name == 'nt':
335                if sys.version_info < (3, 8):
336                        original_dir = os.getcwd()
337                        os.chdir(lib_path)  # because under Windows, frams-objects.dll requires other dll's which reside in the same directory, so we must change current dir for them to be found while loading the main dll.
338                else:
339                        os.add_dll_directory(lib_path)
340        abs_data = os.path.join(os.path.abspath(lib_path), "data")  # use absolute path for -d and -D so python is free to cd anywhere without confusing Framsticks
341
342        # for the hypothetical case without lib_path the abs_data must be obtained from somewhere else
343        if frams_d is None:
344                frams_d = '-d' + abs_data
345        if frams_D is None:
346                frams_D = '-D' + abs_data
347        initargs.insert(0, frams_d)
348        initargs.insert(0, frams_D)
349        initargs.insert(0, 'dummy.exe')  # as an offset, 0th arg is by convention app name
350
351        global c_api  # access global variable
352        if lib_path is not None:  # theoretically, this should only be needed for "and os.name == 'posix'", but in windows python 3.9.5, without using the full lib_name path, there is FileNotFoundError: Could not find module 'frams-objects.dll' (or one of its dependencies). Try using the full path with constructor syntax. Maybe related: https://bugs.python.org/issue42114 and https://stackoverflow.com/questions/59330863/cant-import-dll-module-in-python and https://bugs.python.org/issue39393
353                lib_name = os.path.join(lib_path, lib_name)  # lib_path is always set ('.' when not specified). For the (currently unused) case of lib_path==None, the resulting lib_name is a bare filename without any path, which loads the library from a system-configured location.
354        try:
355                c_api = ctypes.CDLL(lib_name)  # if accessing this module from multiple threads, they will all share a single c_api and access the same copy of the library and its data. If you want separate independent copies, read the comment at the top of this file on using the "multiprocessing" module.
356        except OSError as e:
357                print("*** Could not find or open '%s' from '%s'.\n*** Did you provide proper arguments and is this file readable?\n" % (lib_name, os.getcwd()))
358                raise
359
360        if os.name == 'nt' and sys.version_info < (3, 8):
361                os.chdir(original_dir)  # restore current working dir after loading the library so Framsticks sees the expected directory
362
363        c_api.init.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)]
364        c_api.init.restype = None
365        c_api.extFree.argtypes = [ctypes.c_void_p]
366        c_api.extFree.restype = None
367        c_api.extType.argtypes = [ctypes.c_void_p]
368        c_api.extType.restype = ctypes.c_int
369        c_api.extFromNull.argtypes = []
370        c_api.extFromNull.restype = ctypes.c_void_p
371        c_api.extFromInt.argtypes = [ctypes.c_int]
372        c_api.extFromInt.restype = ctypes.c_void_p
373        c_api.extFromDouble.argtypes = [ctypes.c_double]
374        c_api.extFromDouble.restype = ctypes.c_void_p
375        c_api.extFromString.argtypes = [ctypes.c_char_p]
376        c_api.extFromString.restype = ctypes.c_void_p
377        c_api.extIntValue.argtypes = [ctypes.c_void_p]
378        c_api.extIntValue.restype = ctypes.c_int
379        c_api.extDoubleValue.argtypes = [ctypes.c_void_p]
380        c_api.extDoubleValue.restype = ctypes.c_double
381        c_api.extStringValue.argtypes = [ctypes.c_void_p]
382        c_api.extStringValue.restype = ctypes.c_char_p
383        c_api.extClass.argtypes = [ctypes.c_void_p]
384        c_api.extClass.restype = ctypes.c_char_p
385        c_api.extPropCount.argtypes = [ctypes.c_void_p]
386        c_api.extPropCount.restype = ctypes.c_int
387        c_api.extPropId.argtypes = [ctypes.c_void_p, ctypes.c_int]
388        c_api.extPropId.restype = ctypes.c_char_p
389        c_api.extPropName.argtypes = [ctypes.c_void_p, ctypes.c_int]
390        c_api.extPropName.restype = ctypes.c_char_p
391        c_api.extPropType.argtypes = [ctypes.c_void_p, ctypes.c_int]
392        c_api.extPropType.restype = ctypes.c_char_p
393        c_api.extPropGroup.argtypes = [ctypes.c_void_p, ctypes.c_int]
394        c_api.extPropGroup.restype = ctypes.c_int
395        c_api.extPropFlags.argtypes = [ctypes.c_void_p, ctypes.c_int]
396        c_api.extPropFlags.restype = ctypes.c_int
397        c_api.extPropHelp.argtypes = [ctypes.c_void_p, ctypes.c_int]
398        c_api.extPropHelp.restype = ctypes.c_char_p
399        c_api.extPropFind.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
400        c_api.extPropFind.restype = ctypes.c_int
401        c_api.extPropGet.argtypes = [ctypes.c_void_p, ctypes.c_int]
402        c_api.extPropGet.restype = ctypes.c_void_p
403        c_api.extPropSet.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
404        c_api.extPropSet.restype = ctypes.c_int
405        c_api.extPropCall.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p]
406        c_api.extPropCall.restype = ctypes.c_void_p
407        c_api.extGroupCount.argtypes = [ctypes.c_void_p]
408        c_api.extGroupCount.restype = ctypes.c_int
409        c_api.extGroupName.argtypes = [ctypes.c_void_p, ctypes.c_int]
410        c_api.extGroupName.restype = ctypes.c_char_p
411        c_api.extGroupMember.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
412        c_api.extGroupMember.restype = ctypes.c_int
413        c_api.extMemberCount.argtypes = [ctypes.c_void_p, ctypes.c_int]
414        c_api.extMemberCount.restype = ctypes.c_int
415        c_api.rootObject.argtypes = []
416        c_api.rootObject.restype = ctypes.c_void_p
417
418        c_args = (ctypes.c_char_p * len(initargs))(*list(a.encode(ExtValue._encoding) for a in initargs))
419        c_api.init(len(initargs), c_args)
420
421        Root = ExtValue._rootObject()
422        for n in dir(Root):
423                if n[0].isalpha():
424                        attr = getattr(Root, n)
425                        if isinstance(attr, ExtValue):
426                                attr = attr._value()
427                        setattr(sys.modules[__name__], n, attr)
428
429        print('Using Framsticks version: ' + str(Simulator.version_string))
430        print('Home (writable) dir     : ' + home_dir)
431        print('Resources dir           : ' + res_dir)
432        print()
Note: See TracBrowser for help on using the repository browser.