Skip to content

Commit d0eb687

Browse files
committed
Add win32 support
Make adPython work on Windows. Change how adPython calls into the embedded Python to use PyGILState_Ensure() and PyGILState_Release() which is appropriate for calling from non-Python-created threads which is the case for adPython which is called from areaDetector-created threads. Without this change, adPython hangs on Windows. The Python calling idiom is documented in the following: * https://docs.python.org/2/c-api/init.html#non-python-created-threads * https://stackoverflow.com/a/4975906 * https://bugs.python.org/issue1720250#msg57619 Install the plugin scripts so that adPython can reference an installed pathname rather than a source pathname. Remove the automatic determination of the Python version, include pathnames, library pathnames, etc. by executing external commands from Make since it did not work on Windows. Replace that with explicit definitions in configure/CONFIG_SITE.
1 parent a030175 commit d0eb687

File tree

9 files changed

+175
-57
lines changed

9 files changed

+175
-57
lines changed

adPythonApp/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ DIRS := $(DIRS) $(filter-out $(DIRS), $(wildcard *db*))
66
DIRS := $(DIRS) $(filter-out $(DIRS), $(wildcard *Db*))
77
DIRS := $(DIRS) $(filter-out $(DIRS), $(wildcard *opi*))
88
DIRS := $(DIRS) $(filter-out $(DIRS), $(wildcard protocol))
9+
DIRS := $(DIRS) $(filter-out $(DIRS), $(wildcard scripts))
910
include $(TOP)/configure/RULES_DIRS
1011

adPythonApp/scripts/Makefile

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
TOP=../..
2+
include $(TOP)/configure/CONFIG
3+
#----------------------------------------
4+
# ADD MACRO DEFINITIONS AFTER THIS LINE
5+
6+
#----------------------------------------------------
7+
# Install scripts
8+
PLUGINSCRIPTS += adPythonBarCode.py
9+
PLUGINSCRIPTS += adPythonCircle.py
10+
PLUGINSCRIPTS += adPythonCrystalDroplet.py
11+
PLUGINSCRIPTS += adPythonCrystalFeatureMatch.py
12+
PLUGINSCRIPTS += adPythonDataMatrix.py
13+
PLUGINSCRIPTS += adPythonFocus.py
14+
PLUGINSCRIPTS += adPythonGaussian2DFitter.py
15+
PLUGINSCRIPTS += adPythonMitegen.py
16+
PLUGINSCRIPTS += adPythonMorph.py
17+
PLUGINSCRIPTS += adPythonMxSampleDetect.py
18+
PLUGINSCRIPTS += adPythonPowerMean.py
19+
PLUGINSCRIPTS += adPythonRotate.py
20+
PLUGINSCRIPTS += adPythonTemplate.py
21+
PLUGINSCRIPTS += adPythonTransfer.py
22+
PLUGINSCRIPTS += transferclient.py
23+
24+
include $(TOP)/configure/RULES
25+
#----------------------------------------
26+
# ADD RULES AFTER THIS LINE

adPythonApp/src/Makefile

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@ include $(TOP)/configure/CONFIG
88

99
# This tells the compiler to ignore errors generated by EPICS includes. We need
1010
# this because the EPICS headers have non strict prototypes in places.
11+
ifdef OS_CLASS
12+
ifneq ($(OS_CLASS), WIN32)
1113
USR_CPPFLAGS += -isystem $(EPICS_BASE)/include
14+
endif
15+
endif
1216

1317
# Nice and strict...
18+
USR_CXXFLAGS += -Wall
19+
ifdef OS_CLASS
20+
ifneq ($(OS_CLASS), WIN32)
1421
USR_CXXFLAGS += -Werror
15-
USR_CXXFLAGS += -Wall -Wextra
22+
USR_CXXFLAGS += -Wextra
1623
USR_CXXFLAGS += -Wno-unused-parameter
1724
USR_CXXFLAGS += -Wno-missing-field-initializers
1825
USR_CXXFLAGS += -Wundef
@@ -21,6 +28,8 @@ USR_CXXFLAGS += -Wcast-align
2128
USR_CXXFLAGS += -Wwrite-strings
2229
USR_CXXFLAGS += -Wredundant-decls
2330
USR_CXXFLAGS += -Wmissing-declarations
31+
endif
32+
endif
2433

2534
LIBRARY_IOC += adPython
2635

@@ -30,31 +39,26 @@ DBD += adPythonPlugin.dbd
3039
# The following are compiled and added to the support library
3140
adPython_SRCS += adPythonPlugin.cpp
3241

33-
# Guess the python executable
34-
ifneq ($(PYTHON_PREFIX),)
35-
PYTHON = $(PYTHON_PREFIX)/bin/python
36-
else
37-
PYTHON = python
38-
endif
39-
40-
# Get the version string of python
41-
PYTHON_VERSION = $(shell $(PYTHON) -c 'from distutils import sysconfig; \
42-
print sysconfig.get_config_var("VERSION")')
43-
4442
# link against python
4543
ifneq ($(PYTHON_PREFIX),)
4644
# user defined prefix, add it explicitly
47-
USR_INCLUDES += -I$(PYTHON_PREFIX)/include/python$(PYTHON_VERSION)
48-
python$(PYTHON_VERSION)_DIR = $(PYTHON_PREFIX)/lib
45+
USR_INCLUDES += -I$(PYTHON_INCLUDE)
46+
USR_INCLUDES += -I$(NUMPY_INCLUDE)
47+
python$(PYTHON_VERSION)_DIR = $(PYTHON_LIB)
4948
adPython_LIBS += python$(PYTHON_VERSION)
50-
# add the numpy include path too, which is printed out when we run
51-
# adPythonPlugin.py from the command line
52-
USR_INCLUDES += -I$(shell $(PYTHON) ../adPythonPlugin.py)
5349
else
5450
# it's in a standard place
5551
adPython_SYS_LIBS += python$(PYTHON_VERSION)
5652
endif
5753

58-
USR_CXXFLAGS += -DDATADIRS=\"$(shell cd ..; pwd):$(shell cd ../../scripts; pwd)\"
54+
# add more libs
55+
adPython_LIBS += NDPlugin
56+
57+
# location of plugin scripts
58+
USR_CXXFLAGS += -DDATADIRS=\"$(ADPYTHON_MODULE_PATH)\"
59+
60+
# install plugin scripts
61+
PLUGINSCRIPTS += adPythonOffline.py
62+
PLUGINSCRIPTS += adPythonPlugin.py
5963

6064
include $(TOP)/configure/RULES

adPythonApp/src/adPythonPlugin.cpp

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
99
#include "numpy/ndarrayobject.h"
1010
#include <stdio.h>
11-
#include <libgen.h>
1211
#include <epicsTime.h>
1312
#include "NDArray.h"
1413
#include "adPythonPlugin.h"
@@ -34,14 +33,16 @@
3433
#define Bad(errString) NoGood(errString, BAD)
3534
#define Ugly(errString) NoGood(errString, UGLY)
3635

36+
// Used in setting the python module search path
37+
#ifdef _WIN32
38+
#define PATH_LIST_SEPARATOR ";"
39+
#else
40+
#define PATH_LIST_SEPARATOR ":"
41+
#endif
42+
3743
// Used in error printing
3844
const char *driverName = "adPythonPlugin";
3945

40-
// This holds the threadState of the thread that initialised python
41-
// We need it to get a handle on the interpreter so create a threadState
42-
// object for each port
43-
static PyThreadState *mainThreadState = NULL;
44-
4546
adPythonPlugin::adPythonPlugin(const char *portNameArg, const char *filename,
4647
const char *classname, int queueSize, int blockingCallbacks,
4748
const char *NDArrayPort, int NDArrayAddr, int maxBuffers,
@@ -81,24 +82,24 @@ adPythonPlugin::adPythonPlugin(const char *portNameArg, const char *filename,
8182
*/
8283
void adPythonPlugin::initThreads()
8384
{
84-
// First we tell python where to find adPythonPlugin.py and other scripts
85-
char buffer[BIGBUFFER];
86-
snprintf(buffer, sizeof(buffer), "PYTHONPATH=%s", DATADIRS);
87-
putenv(buffer);
88-
89-
// Now we initialise python
85+
PyGILState_STATE gstate;
86+
87+
// Initialise python
9088
if (!Py_IsInitialized()) {
9189
PyEval_InitThreads();
9290
Py_Initialize();
93-
94-
// Be sure to save thread state to release the GIL and give a handle
95-
// on the interpreter to this and other ports
96-
mainThreadState = PyEval_SaveThread();
91+
// Release the GIL
92+
PyEval_SaveThread();
9793
}
9894

99-
// Create a thread state just for us
100-
this->threadState = PyThreadState_New(mainThreadState->interp);
101-
PyEval_RestoreThread(this->threadState);
95+
// Acquire the GIL and set up non-python-created thread access
96+
gstate = PyGILState_Ensure();
97+
98+
// Tell python where to find adPythonPlugin.py and other scripts
99+
char buffer[BIGBUFFER];
100+
snprintf(buffer, sizeof(buffer), "%s%s%s", Py_GetPath(),
101+
PATH_LIST_SEPARATOR, DATADIRS);
102+
PySys_SetPath(buffer);
102103

103104
// Import our supporting library
104105
this->importAdPythonModule();
@@ -117,8 +118,8 @@ void adPythonPlugin::initThreads()
117118
this->updateParamList(1);
118119
}
119120

120-
// Release the GIL and finish
121-
this->threadState = PyEval_SaveThread();
121+
// Release the GIL and tear down non-python-created thread access
122+
PyGILState_Release(gstate);
122123
}
123124

124125
/** Callback function that is called by the NDArray driver with new NDArray data
@@ -130,6 +131,8 @@ void adPythonPlugin::initThreads()
130131
* Called with this->lock taken
131132
*/
132133
void adPythonPlugin::processCallbacks(NDArray *pArray) {
134+
PyGILState_STATE gstate;
135+
133136
// First call the base class method
134137
NDPluginDriver::processCallbacks(pArray);
135138

@@ -143,8 +146,8 @@ void adPythonPlugin::processCallbacks(NDArray *pArray) {
143146
this->unlock();
144147
epicsMutexLock(this->dictMutex);
145148

146-
// Make sure we're allowed to use the python API
147-
PyEval_RestoreThread(this->threadState);
149+
// Acquire the GIL and set up non-python-created thread access
150+
gstate = PyGILState_Ensure();
148151
this->lock();
149152

150153
// Store the time at the beginning of processing for profiling
@@ -181,9 +184,11 @@ void adPythonPlugin::processCallbacks(NDArray *pArray) {
181184
setDoubleParam(adPythonTime, epicsTimeDiffInSeconds(&end, &start)*1000);
182185
callParamCallbacks();
183186

184-
// release GIL and dict Mutex
187+
// Unlock
185188
this->unlock();
186-
this->threadState = PyEval_SaveThread();
189+
// Release the GIL and tear down non-python-created thread access
190+
PyGILState_Release(gstate);
191+
// Release dict mutex
187192
epicsMutexUnlock(this->dictMutex);
188193

189194
// Spit out the array
@@ -209,14 +214,15 @@ asynStatus adPythonPlugin::writeInt32(asynUser *pasynUser, epicsInt32 value) {
209214
int param = pasynUser->reason;
210215
if (param == adPythonLoad ||
211216
(this->nextParam && param >= adPythonUserParams[0])) {
217+
PyGILState_STATE gstate;
212218
// We have to modify our python dict to match our param list
213219
// Note: to avoid deadlocks we should always take locks in order:
214220
// dictMutex, then GIL, then this->lock
215221
// so unlock here to preserve this order
216222
this->unlock();
217223
epicsMutexLock(this->dictMutex);
218-
// Make sure we're allowed to use the python API
219-
PyEval_RestoreThread(this->threadState);
224+
// Acquire the GIL and set up non-python-created thread access
225+
gstate = PyGILState_Ensure();
220226
// Now call the bast class to write the value to the param list
221227
this->lock();
222228
status |= NDPluginDriver::writeInt32(pasynUser, value);
@@ -230,8 +236,9 @@ asynStatus adPythonPlugin::writeInt32(asynUser *pasynUser, epicsInt32 value) {
230236
// our param lib has changed, so update the dict and reprocess
231237
status |= this->updateParamDict();
232238
}
233-
// release GIL and dict Mutex
234-
this->threadState = PyEval_SaveThread();
239+
// Release the GIL and tear down non-python-created thread access
240+
PyGILState_Release(gstate);
241+
// Release dict mutex
235242
epicsMutexUnlock(this->dictMutex);
236243
} else {
237244
status |= NDPluginDriver::writeInt32(pasynUser, value);
@@ -252,21 +259,23 @@ asynStatus adPythonPlugin::writeFloat64(asynUser *pasynUser,
252259
int status = asynSuccess;
253260
int param = pasynUser->reason;
254261
if (this->nextParam && param >= adPythonUserParams[0]) {
262+
PyGILState_STATE gstate;
255263
// We have to modify our python dict to match our param list
256264
// Note: to avoid deadlocks we should always take locks in order:
257265
// dictMutex, then GIL, then this->lock
258266
// so unlock here to preserve this order
259267
this->unlock();
260268
epicsMutexLock(this->dictMutex);
261-
// Make sure we're allowed to use the python API
262-
PyEval_RestoreThread(this->threadState);
269+
// Acquire the GIL and set up non-python-created thread access
270+
gstate = PyGILState_Ensure();
263271
// Now call the bast class to write the value to the param list
264272
this->lock();
265273
status |= NDPluginDriver::writeFloat64(pasynUser, value);
266274
// our param lib has changed, so update the dict and reprocess
267275
status |= this->updateParamDict();
268-
// release GIL and dict Mutex
269-
this->threadState = PyEval_SaveThread();
276+
// Release the GIL and tear down non-python-created thread access
277+
PyGILState_Release(gstate);
278+
// Release dict mutex
270279
epicsMutexUnlock(this->dictMutex);
271280
} else {
272281
status = NDPluginDriver::writeFloat64(pasynUser, value);
@@ -289,21 +298,23 @@ asynStatus adPythonPlugin::writeOctet(asynUser *pasynUser, const char *value,
289298
int status = asynSuccess;
290299
int param = pasynUser->reason;
291300
if (this->nextParam && param >= adPythonUserParams[0]) {
301+
PyGILState_STATE gstate;
292302
// We have to modify our python dict to match our param list
293303
// Note: to avoid deadlocks we should always take locks in order:
294304
// dictMutex, then GIL, then this->lock
295305
// so unlock here to preserve this order
296306
this->unlock();
297307
epicsMutexLock(this->dictMutex);
298-
// Make sure we're allowed to use the python API
299-
PyEval_RestoreThread(this->threadState);
308+
// Acquire the GIL and set up non-python-created thread access
309+
gstate = PyGILState_Ensure();
300310
// Now call the bast class to write the value to the param list
301311
this->lock();
302312
status |= NDPluginDriver::writeOctet(pasynUser, value, maxChars, nActual);
303313
// our param lib has changed, so update the dict and reprocess
304314
status |= this->updateParamDict();
305-
// release GIL and dict Mutex
306-
this->threadState = PyEval_SaveThread();
315+
// Release the GIL and tear down non-python-created thread access
316+
PyGILState_Release(gstate);
317+
// Release dict mutex
307318
epicsMutexUnlock(this->dictMutex);
308319
} else {
309320
status |= NDPluginDriver::writeOctet(pasynUser, value, maxChars, nActual);

adPythonApp/src/adPythonPlugin.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
#include <iocsh.h>
77
#include <epicsExport.h>
88

9+
#if defined(_MSC_VER) && !defined(__func__)
10+
#define __func__ __FUNCTION__
11+
#endif
12+
913
// Max number of user parameters in a subclass
1014
#define NUSERPARAMS 100
1115

@@ -58,7 +62,6 @@ class adPythonPlugin : public NDPluginDriver {
5862
NDAttributeList *pFileAttributes;
5963
int nextParam, pluginState;
6064
epicsMutexId dictMutex;
61-
PyThreadState *threadState;
6265
};
6366

6467
#endif

configure/CONFIG_PLUGINSCRIPTS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FILE_TYPE += PLUGINSCRIPTS
2+
INSTALL_PLUGINSCRIPTS = $(INSTALL_LOCATION)/plugin-scripts

0 commit comments

Comments
 (0)