Repository: numpy/numpy

Issue:
Going from numpy 1.14.5 to 1.15.0rc1 breaks loading a DLL using ctypes
In our [Python API](https://github.com/ni/nimi-python) for accessing measurement hardware we allow the customer to retrieve their measurements in `numpy` arrays in case they want to do additional processing.

When using `numpy` 1.14.5 or earlier, we are able to load our C DLL with no issue and make the appropriate calls using `ctypes`.

When using `numpy` 1.15.0rc1, we get an error when `ctypes` tries to load the DLL saying "[WinError 126] The specified module could not be found".

I am using `tox` to run tests so it is using `venv` and all I have to do is change the version of `numpy` and it will start working. This means the DLL is in the path as expected and all dependencies are also loadable.

I have verified that `numpy` is the only installed package whose version is changing.

I have verified that the path environment variable has the appropriate directory even in the failure case.

I have verified that this happens on Python 2.7 and 3.6, both 32 bit and 64 bit.

As far as I know we are using `ctypes` correctly. At this point I don't have any more ideas on what to look at.


Discussion:
Thanks for the report. I assume you are installing the version on PyPI with pip?
Assuming the dll is found on the path, Windows emits this error when there are missing dependencies. Could you check if [depends.exe](http://dependencywalker.com) shows any problems?
I am installing via pip from PyPI.

The DLL is on the path, and other applications can use the DLL without a problem. Also, just by changing from 1.15.0rc1 to 1.14.5 will allow Python to work as well.

Dependency Walker shows no issues.

I do have more information. This happens if I try to load the DLL after importing `numpy`. If I load the DLL and then import numpy, then it all works. So it seems that `import numpy` changes something somewhere. I looked at a couple of the `__init__.py` files, but nothing jumped out at me and I am not familiar with the internals of numpy.
I assume you are not compiling numpy. What have you used to produce the DLL? I note that we have had some problems traceable to import order.
I am not compiling numpy.

Our DLL is built with msvc 14.0, and is dynamically linked to msvcp140.dll and vcruntime140.dll.

We do not load our DLL at import time, but at the first time the main class is instantiated. If that matters.
There have been some changes to `numpy/ctypeslib.py`, but I don't see any changes that should affect the loading.  Is the `loader_path` the same as before. @eric-wieser Thoughts?
Are you working in a virtual environment?
I am using tox to run tests, which uses venv to create virtual environments.

How do I check the `loader_path`?
When you say "load the dll" what is the exact code that does this? I could not find "multiarray" in a quck search of your linked repo.
We use `numpy.ndarray`.

The actual code to load our dll looks like
``` python
try:                                                                 
    library_type = _get_library_type()                               
    if library_type == 'windll':                                     
        ctypes_library = ctypes.WinDLL(_get_library_name())          
    else:                                                            
        assert library_type == 'cdll'                                
        ctypes_library = ctypes.CDLL(_get_library_name())            
except OSError:                                                 
    raise errors.DriverNotInstalledError()                           
```
where `_get_library_name()` will return the name with no path information. One driver that is failing the `library_type` will be 'windll' on x86 and 'cdll' on x64, and the name will be 'nidmm_32.dll' or 'nidmm_64.dll'
It is an argument to `ctypes_load_library`. How are you loading the library for ctypes?
I do not use `ctypes_load_library` for anything. I use `numpy.ctypeslib.as_ctypes()` as needed to get a ctypes pointer to a `numpy.ndarray` that I then pass into my DLL. See my previous comment for how we load our DLL.
As a side question, what is the difference between `WinDLL` and `CDLL`? I ask, as the only function we, as opposed to you, use is `ctypes.cdll[libpath]`, so I wonder if we are missing something.
@charris the difference is in the assumption about the [calling convention](https://en.wikipedia.org/wiki/X86_calling_conventions) when setting up the stack in calling functions from the DLL
OK, so AFAICT, you are saying that the numpy functions are not failing, but that somehow numpy messes with your import function. It would be lovely if you could run a bisect for this, but I don't suppose that is possible.
@texasaggie97 so the error actually occurs when loading `nidmm*.dll` so something like ' `ctypes.WinDLL('nidmm_32.dll')`? Does that dll link to different versions of support libraries than numpy's `multiarray.dll` (so depends shows they both depend on the same dll)? Perhaps when `multiarray.dll` is loaded first, it prefers a version of a dll in its directory, and `nidmm` needs functions that do not appear in the one loaded?

Edit: formatting
@charris yes, that is what I am seeing.

Where is multiarray.dll installed? Searching my entire drive came up with nothing.

Looking in the virtual environment, the only DLL I see is libopenblas...dll. However, both the 32 bit venv and the 64 bit venv have libopenblas...amd64.dll. And it does seem to be a 64 bit DLL since a 32 bit Dependency Walker see all kinds of errors and the 64 bit version of dependency walker has no problems.
Sorry, I forgot on windows it is called `multiarray.pyd` which is just a dll renamed for python
I also tried to install the vendor toolkit for `nidmm` but it is not freely available
The `libopenblas` thing may be unrelated, but looks wrong. That build is new, and the 32 bit library should be coming from http://wheels.scipy.org/   `openblas-v0.3.0-win32-gcc_7_1_0.zip`.  I think that should be an honest 32 bit lib as our tests passed with it.  However, perhaps you have only installed the 64 bit version from pip? @matthew-brett Ping, just in case something odd is going on.
Our Python support is available from PyPI so `pip install nidmm` should work. The driver DLL is part of a separate (large) [download](http://www.ni.com/download/ni-dmm-18.1/7608/en/).

Nevermind about the 32 bit vs 64 bit. Even though `tox` says it is running 32 bit Python, I checked the other binaries in the venv cache and they are indeed 64 bit. This means that the DLL name to load will be 'nidmm_64.dll' instead and it will be of type 'cdll'. 

The error is coming from `ctypes.CDLL('nidmm_64.dll')`. I added some additional debugging output to verify this.

I don't see any common dependencies between 'multiarray.pyd' and 'nidmm_64.dll' other than kernel32.dll which should be fine. 'multiarray.pyd' uses the msvc 9.0 runtime and 'nidmm_64.dll' uses the msvc 14.0 runtime.
As NumPy has not changed msvc runtime versions between 1.14 and 1.15, it is unlikely this is the problem
I reproduced a similar issue with PyQt5.
With a fresh install of Python3.6.6_x64 from Python.org on Windows 7 Professional SP1, then install numpy 1.15.0rc1 and PyQt5 from pypi wheels, runinng:
```
import numpy
import PyQt5.QtCore
```
leads to
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: DLL load failed: The specified module could not be found.
```
, while 
```
import PyQt5.QtCore
import numpy
```
works fine.

I then tested with numpy built from source (`pip install --pre --no-binary numpy numpy`) and the issue disappear.
@t20100 Thanks for the info. At this point it sounds like a problem related to NumPy using the OpenBLAS DLL. Probably not OpenBLAS itself, but perhaps something related to the runtime linkage. I'm not at all familiar with how windows does things. @cgohlke @eric-wieser Any ideas?

I note that Christoph also provides windows wheels for 1.15.0rc1 linked against MKL at https://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy , might be worth testing those also.
I cannot reproduce the error with the wheel from https://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy
I think the culprit is the call to [`SetDefaultDllDirectories`](https://github.com/numpy/numpy/blob/master/numpy/distutils/misc_util.py#L2309), which IIUC removes the current working directory and all directories in the `PATH` environment variable from the process DLL search path.
Maybe we should back that out? The relevant PR was #10229, and even after that was merged there was discussion that maybe it was not the best solution. It was put in to solve a scipy issue that others could not replicate? Now it seems we are breaking downstream packages
Hmm, that is new in 1.15. @cgohlke I'm guessing that you don't use that in your builds.
> I'm guessing that you don't use that in your builds.

Correct. `numpy+mkl` doesn't have a `numpy/.libs` directory. 
@texasaggie97, @t20100 could you verify that reverting that PR (you can just modify the pure-python code in numpy/distutils/misc_util.py) solves the problems and doesn't cause new ones?
@mattip There wasn't a problem with the homebuilt versions, so it looks to be build environment related. From the comment, probably the combination of a gfortran library and msvc.
I'm a little curious why there is a dll to start with, I thought we were statically linked. Of course, if fortran is involved, there is probably a fortran runtime somewhere.

For testing a fix, probably we want to do it in the nightly builds of numpy-wheels, which I think we can ping anytime. Not sure of the best way to deal with the modifications themselves, maybe a temporary testing branch in the numpy repo. @matthew-brett Ideas?
I had to change the `__config__.py` file in the top level `numpy` directory, but then it worked with no error.
FWIW, scipy wheels do only `os.environ["PATH"] += os.pathsep + extra_dll_dir` instead of calling SetDefaultDllDirectories, so even if there was some scipy motivation for the change (it is not that clear there was a good motivation if it's crt bug), it is not made use of currently.
@cgohlke Fabulous detective work.

Reverting #10229 looks like a good idea to me. Conceptually I think `SetDefaultDllDirectories` is the wrong thing to use anyway, and from the discussion in that PR the motivation for adding it was *extremely* dubious.

Resolving patch:
diff --git a/numpy/distutils/misc_util.py b/numpy/distutils/misc_util.py
--- a/numpy/distutils/misc_util.py
+++ b/numpy/distutils/misc_util.py
@@ -2300,19 +2300,9 @@ def generate_config_py(target):
 
 extra_dll_dir = os.path.join(os.path.dirname(__file__), '.libs')
 
-if os.path.isdir(extra_dll_dir) and sys.platform == 'win32':
-    try:
-        from ctypes import windll, c_wchar_p
-        _AddDllDirectory = windll.kernel32.AddDllDirectory
-        _AddDllDirectory.argtypes = [c_wchar_p]
-        # Needed to initialize AddDllDirectory modifications
-        windll.kernel32.SetDefaultDllDirectories(0x1000)
-    except AttributeError:
-        def _AddDllDirectory(dll_directory):
-            os.environ.setdefault('PATH', '')
-            os.environ['PATH'] += os.pathsep + dll_directory
-
-    _AddDllDirectory(extra_dll_dir)
+if sys.platform == 'win32' and os.path.isdir(extra_dll_dir):
+    os.environ.setdefault('PATH', '')
+    os.environ['PATH'] += os.pathsep + extra_dll_dir
 
 """)
 


Test patch: