source: framspy/frams.py @ 1343

Last change on this file since 1343 was 1343, checked in by Maciej Komosinski, 9 days ago

Protect against the "c_api" global variable becoming None when Python runtime is terminating, but some ExtValue? objects are still about to be destroyed

File size: 16.3 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
20If you want to use this module from multiple threads concurrently, use the "-t" option for init().
21This will make concurrent calls from different threads sequential, thus making them safe.
22However, this will likely degrade the performance (due to required locking) compared to the single-threaded use.
23
24For interfaces in other languages (e.g. using the Framsticks library in your C++ code), see ../cpp/frams/frams-objects.h
25"""
26
27import ctypes, re, sys, os
28
29c_api = None  # will be initialized in init(). Global because ExtValue uses it.
30
31
32class ExtValue(object):
33        """All Framsticks objects and values are instances of this class. Read the documentation of the 'frams' module for more information."""
34
35        _reInsideParens = re.compile('\((.*)\)')
36        _reservedWords = ['import']  # this list is scanned during every attribute access, only add what is really clashing with Framsticks properties
37        _reservedXWords = ['x' + word for word in _reservedWords]
38        _encoding = 'utf-8'
39
40
41        def __init__(self, arg=None, dontinit=False):
42                if dontinit:
43                        return
44                       
45                if isinstance(arg, int):
46                        self._initFromInt(arg)
47                elif isinstance(arg, str):
48                        self._initFromString(arg)
49                elif isinstance(arg, float):
50                        self._initFromDouble(arg)
51                elif arg == None:
52                        self._initFromNull()
53                else:
54                        raise ctypes.ArgumentError("Can't make ExtValue from '%s' (%s)" % (str(arg), type(arg)))
55
56                # Bypass our custom __setattr__ (just like a regular self.myfield=... assignment here, it would cause infinite recursion because our custom __setattr__ creates another ExtValue object) by calling object.__setattr__ directly:
57                # object.__setattr__(self, 'debuginfo', "typ=%s, class=%s, arg=%s" % (str(self._type()), str(self._class()), str(arg)))  # for debugging the order of deletion/destruction of ExtValue objects         
58
59
60        def __del__(self):
61                # debuginfo = self.__dict__['debuginfo'] if 'debuginfo' in self.__dict__ else '(not-inited)'
62                if c_api is not None:  # there is some unknown interaction between the native Framsticks library and other native Python libraries (like numpy) which affects the order of Python interpreter's garbage collector and leads to occasional calls of this destructor after c_api becomes None (when the Python interpreter exits). Hence this protection to avoid calling extFree() on None. An alternative would be to use self._finalizer = weakref.finalize(self, c_api.extFree, self.__ptr) instead of __del__.
63                        #print("\tDeleter of the object with debuginfo='%s'" % debuginfo)
64                        c_api.extFree(self.__ptr)
65                #else:
66                #       print("\t*** The deleter of the object with debuginfo='%s' has c_api==None, so unable to extFree() !" % debuginfo)
67
68
69        def _initFromNull(self):
70                self.__ptr = c_api.extFromNull()
71
72
73        def _initFromInt(self, v):
74                self.__ptr = c_api.extFromInt(v)
75
76
77        def _initFromDouble(self, v):
78                self.__ptr = c_api.extFromDouble(v)
79
80
81        def _initFromString(self, v):
82                self.__ptr = c_api.extFromString(ExtValue._cstringFromPython(v))
83
84
85        @classmethod
86        def _makeNull(cls, v):
87                e = ExtValue(None, True)
88                e._initFromNull()
89                return e
90
91
92        @classmethod
93        def _makeInt(cls, v):
94                e = ExtValue(None, True)
95                e._initFromInt(v)
96                return e
97
98
99        @classmethod
100        def _makeDouble(cls, v):
101                e = ExtValue(None, True)
102                e._initFromDouble(v)
103                return e
104
105
106        @classmethod
107        def _makeString(cls, v):
108                e = ExtValue(None, True)
109                e._initFromString(v)
110                return e
111
112
113        @classmethod
114        def _rootObject(cls):
115                e = ExtValue(None, True)
116                e.__ptr = c_api.rootObject()
117                return e
118
119
120        @classmethod
121        def _stringFromC(cls, cptr):
122                return cptr.decode(ExtValue._encoding)
123
124
125        @classmethod
126        def _cstringFromPython(cls, s):
127                return ctypes.c_char_p(s.encode(ExtValue._encoding))
128
129
130        def _type(self):
131                return c_api.extType(self.__ptr)
132
133
134        def _class(self):
135                cls = c_api.extClass(self.__ptr)
136                if cls == None:
137                        return None
138                else:
139                        return ExtValue._stringFromC(cls)
140
141
142        def _value(self):
143                t = self._type()
144                if t == 0:
145                        return None
146                elif t == 1:
147                        return self._int()
148                elif t == 2:
149                        return self._double()
150                elif t == 3:
151                        return self._string()
152                else:
153                        return self
154
155
156        def _int(self):
157                return c_api.extIntValue(self.__ptr)
158
159
160        def _double(self):
161                return c_api.extDoubleValue(self.__ptr)
162
163
164        def _string(self):
165                return ExtValue._stringFromC(c_api.extStringValue(self.__ptr))
166
167
168        def _propCount(self):
169                return c_api.extPropCount(self.__ptr)
170
171
172        def _propId(self, i):
173                return ExtValue._stringFromC(c_api.extPropId(self.__ptr, i))
174
175
176        def _propName(self, i):
177                return ExtValue._stringFromC(c_api.extPropName(self.__ptr, i))
178
179
180        def _propType(self, i):
181                return ExtValue._stringFromC(c_api.extPropType(self.__ptr, i))
182
183
184        def _propHelp(self, i):
185                h = c_api.extPropHelp(self.__ptr, i)  # unlike other string fields, help is sometimes NULL
186                return ExtValue._stringFromC(h) if h != None else '';
187
188
189        def _propFlags(self, i):
190                return c_api.extPropFlags(self.__ptr, i)
191
192
193        def _propGroup(self, i):
194                return c_api.extPropGroup(self.__ptr, i)
195
196
197        def _groupCount(self):
198                return c_api.extGroupCount(self.__ptr)
199
200
201        def _groupName(self, i):
202                return ExtValue._stringFromC(c_api.extGroupName(self.__ptr, i))
203
204
205        def _groupMember(self, g, i):
206                return c_api.extGroupMember(self.__ptr, g, i)
207
208
209        def _memberCount(self, g):
210                return c_api.extMemberCount(self.__ptr, g)
211
212
213        def __str__(self):
214                return self._string()
215
216
217        def __dir__(self):
218                ids = dir(type(self))
219                if self._type() == 4:
220                        for i in range(c_api.extPropCount(self.__ptr)):
221                                name = ExtValue._stringFromC(c_api.extPropId(self.__ptr, i))
222                                if name in ExtValue._reservedWords:
223                                        name = 'x' + name
224                                ids.append(name)
225                return ids
226
227
228        def __getattr__(self, key):
229                if key[0] == '_':
230                        return self.__dict__[key]
231                if key in ExtValue._reservedXWords:
232                        key = key[1:]
233                prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
234                if prop_i < 0:
235                        raise AttributeError('no ' + str(key) + ' in ' + str(self))
236                t = ExtValue._stringFromC(c_api.extPropType(self.__ptr, prop_i))
237                if t[0] == 'p':
238                        arg_types = ExtValue._reInsideParens.search(t)
239                        if arg_types:
240                                arg_types = arg_types.group(1).split(',')  # anyone wants to add argument type validation using param type declarations?
241
242
243                        def fun(*args):
244                                ext_args = []
245                                ext_pointers = []
246                                for a in args:
247                                        if isinstance(a, ExtValue):
248                                                ext = a
249                                        else:
250                                                ext = ExtValue(a)
251                                        ext_args.append(ext)
252                                        ext_pointers.append(ext.__ptr)
253                                ret = ExtValue(None, True)
254                                args_array = (ctypes.c_void_p * len(args))(*ext_pointers)
255                                ret.__ptr = c_api.extPropCall(self.__ptr, prop_i, len(args), args_array)
256                                return ret
257
258
259                        return fun
260                else:
261                        ret = ExtValue(None, True)
262                        ret.__ptr = c_api.extPropGet(self.__ptr, prop_i)
263                        return ret
264
265
266        def __setattr__(self, key, value):
267                if key[0] == '_':
268                        self.__dict__[key] = value
269                else:
270                        if key in ExtValue._reservedXWords:
271                                key = key[1:]
272                        prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
273                        if prop_i < 0:
274                                raise AttributeError("No '" + str(key) + "' in '" + str(self) + "'")
275                        if not isinstance(value, ExtValue):
276                                value = ExtValue(value)
277                        c_api.extPropSet(self.__ptr, prop_i, value.__ptr)
278
279
280        def __getitem__(self, key):
281                return self.get(key)
282
283
284        def __setitem__(self, key, value):
285                return self.set(key, value)
286
287
288        def __len__(self):
289                try:
290                        return self.size._int()
291                except:
292                        return 0
293
294
295        def __iter__(self):
296                class It(object):
297                        def __init__(self, container, frams_it):
298                                self.container = container
299                                self.frams_it = frams_it
300
301
302                        def __iter__(self):
303                                return self
304
305
306                        def __next__(self):
307                                if self.frams_it.next._int() != 0:
308                                        return self.frams_it.value
309                                else:
310                                        raise StopIteration()
311
312                return It(self, self.iterator)
313
314
315def init(*args):
316        """
317        Initializes the connection to Framsticks dll/so/dylib.
318
319        Python programs do not have to know the Framstics path but if they know, just pass the path as the first argument.
320        Similarly '-dPATH' and '-DPATH' needed by Framsticks are optional and derived from the first path, unless they are specified as args in init().
321        '-LNAME' is the optional library name (full name including the file name extension), default is 'frams-objects.dll/.so/.dylib' depending on the platform.
322        All other arguments are passed to Framsticks and not interpreted by this function.
323        """
324
325        frams_d = None
326        frams_D = None
327        lib_path = None
328        lib_name = ('frams-objects.dylib' if sys.platform == 'darwin' else 'frams-objects.so') if os.name == 'posix' else 'frams-objects.dll'
329        initargs = []
330        for a in args:
331                if a[:2] == '-d':
332                        frams_d = a
333                elif a[:2] == '-D':
334                        frams_D = a
335                elif a[:2] == '-L':
336                        lib_name = a[2:]
337                elif a[:2] == '-t':
338                        print("frams.py: thread synchronization enabled.")  # Due to performance penalty, only use if you are really calling methods from different threads.
339                        from functools import wraps
340                        from threading import RLock
341                       
342                        def threads_synchronized(lock):
343                                def wrapper(f):
344                                        @wraps(f)
345                                        def inner_wrapper(*args, **kwargs):
346                                                with lock:
347                                                        return f(*args, **kwargs)
348                                        return inner_wrapper
349                                return wrapper
350
351                        thread_synchronizer = threads_synchronized(RLock())
352                        for name in ExtValue.__dict__:
353                                attr = getattr(ExtValue, name)
354                                if callable(attr) and attr:  # decorate all methods of ExtValue with a reentrant lock so that different threads do not use them concurrently
355                                        setattr(ExtValue, name, thread_synchronizer(attr))
356                elif lib_path is None:
357                        lib_path = a
358                else:
359                        initargs.append(a)
360        if lib_path is None:
361                # TODO: use environment variable and/or the zip distribution we are in when the path is not specified in arg
362                # for now just assume the current dir is Framsticks
363                lib_path = '.'
364
365        if os.name == 'nt':
366                if sys.version_info < (3, 8):
367                        original_dir = os.getcwd()
368                        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.
369                else:
370                        os.add_dll_directory(os.path.abspath(lib_path))
371        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
372
373        # for the hypothetical case without lib_path, the abs_data must be obtained from somewhere else
374        if frams_d is None:
375                frams_d = '-d' + abs_data
376        if frams_D is None:
377                frams_D = '-D' + abs_data
378        initargs.insert(0, frams_d)
379        initargs.insert(0, frams_D)
380        initargs.insert(0, 'dummy.exe')  # as an offset, 0th arg is by convention app name
381
382        global c_api  # access global variable
383        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
384                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.
385        try:
386                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.
387        except OSError as e:
388                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()))
389                raise
390
391        if os.name == 'nt' and sys.version_info < (3, 8):
392                os.chdir(original_dir)  # restore current working dir after loading the library so Framsticks sees the expected directory
393
394        c_api.init.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)]
395        c_api.init.restype = None
396        c_api.extFree.argtypes = [ctypes.c_void_p]
397        c_api.extFree.restype = None
398        c_api.extType.argtypes = [ctypes.c_void_p]
399        c_api.extType.restype = ctypes.c_int
400        c_api.extFromNull.argtypes = []
401        c_api.extFromNull.restype = ctypes.c_void_p
402        c_api.extFromInt.argtypes = [ctypes.c_int]
403        c_api.extFromInt.restype = ctypes.c_void_p
404        c_api.extFromDouble.argtypes = [ctypes.c_double]
405        c_api.extFromDouble.restype = ctypes.c_void_p
406        c_api.extFromString.argtypes = [ctypes.c_char_p]
407        c_api.extFromString.restype = ctypes.c_void_p
408        c_api.extIntValue.argtypes = [ctypes.c_void_p]
409        c_api.extIntValue.restype = ctypes.c_int
410        c_api.extDoubleValue.argtypes = [ctypes.c_void_p]
411        c_api.extDoubleValue.restype = ctypes.c_double
412        c_api.extStringValue.argtypes = [ctypes.c_void_p]
413        c_api.extStringValue.restype = ctypes.c_char_p
414        c_api.extClass.argtypes = [ctypes.c_void_p]
415        c_api.extClass.restype = ctypes.c_char_p
416        c_api.extPropCount.argtypes = [ctypes.c_void_p]
417        c_api.extPropCount.restype = ctypes.c_int
418        c_api.extPropId.argtypes = [ctypes.c_void_p, ctypes.c_int]
419        c_api.extPropId.restype = ctypes.c_char_p
420        c_api.extPropName.argtypes = [ctypes.c_void_p, ctypes.c_int]
421        c_api.extPropName.restype = ctypes.c_char_p
422        c_api.extPropType.argtypes = [ctypes.c_void_p, ctypes.c_int]
423        c_api.extPropType.restype = ctypes.c_char_p
424        c_api.extPropGroup.argtypes = [ctypes.c_void_p, ctypes.c_int]
425        c_api.extPropGroup.restype = ctypes.c_int
426        c_api.extPropFlags.argtypes = [ctypes.c_void_p, ctypes.c_int]
427        c_api.extPropFlags.restype = ctypes.c_int
428        c_api.extPropHelp.argtypes = [ctypes.c_void_p, ctypes.c_int]
429        c_api.extPropHelp.restype = ctypes.c_char_p
430        c_api.extPropFind.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
431        c_api.extPropFind.restype = ctypes.c_int
432        c_api.extPropGet.argtypes = [ctypes.c_void_p, ctypes.c_int]
433        c_api.extPropGet.restype = ctypes.c_void_p
434        c_api.extPropSet.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
435        c_api.extPropSet.restype = ctypes.c_int
436        c_api.extPropCall.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p]
437        c_api.extPropCall.restype = ctypes.c_void_p
438        c_api.extGroupCount.argtypes = [ctypes.c_void_p]
439        c_api.extGroupCount.restype = ctypes.c_int
440        c_api.extGroupName.argtypes = [ctypes.c_void_p, ctypes.c_int]
441        c_api.extGroupName.restype = ctypes.c_char_p
442        c_api.extGroupMember.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
443        c_api.extGroupMember.restype = ctypes.c_int
444        c_api.extMemberCount.argtypes = [ctypes.c_void_p, ctypes.c_int]
445        c_api.extMemberCount.restype = ctypes.c_int
446        c_api.rootObject.argtypes = []
447        c_api.rootObject.restype = ctypes.c_void_p
448
449        c_args = (ctypes.c_char_p * len(initargs))(*list(a.encode(ExtValue._encoding) for a in initargs))
450        c_api.init(len(initargs), c_args)
451
452        Root = ExtValue._rootObject()
453        for n in dir(Root):
454                if n[0].isalpha():
455                        attr = getattr(Root, n)
456                        if isinstance(attr, ExtValue):
457                                attr = attr._value()
458                        setattr(sys.modules[__name__], n, attr)
459
460        print('Using Framsticks version: ' + str(Simulator.version_string))
461        print('Home (writable) dir     : ' + home_dir)
462        print('Resources dir           : ' + res_dir)
463        print()
Note: See TracBrowser for help on using the repository browser.