Skip to content

Conversation

@ghost
Copy link

@ghost ghost commented Dec 17, 2017

This should reduce some DLL hell scenarios, namely scipy/scipy#8064.

partial credits to @carlkl

@ghost ghost changed the title BUG: distutils: load all DLLs on import BUG: distutils: fix extra DLL loading in certain scenarios Dec 17, 2017
@carlkl
Copy link
Member

carlkl commented Dec 17, 2017

is this working with freeze? (py2exe, cxfreeze, ..)

@ghost
Copy link
Author

ghost commented Dec 17, 2017

See pyinstaller/pyinstaller#3048.

This should reduce some DLL hell scenarios, namely scipy/scipy#8064.

partial credits to @carlkl
@charris
Copy link
Member

charris commented Jan 2, 2018

Let's give this a try. Thanks @xoviat .

@charris charris merged commit f1d81d8 into numpy:master Jan 2, 2018
@ghost ghost deleted the config-load-dll branch January 2, 2018 01:21
@ghost
Copy link
Author

ghost commented Jan 2, 2018

Hrm... seems this breaks editable installations when using conda (while it fixes the bug mentioned). @isuruf what can we do about this; I think it's an issue with conda?

@ghost
Copy link
Author

ghost commented Jan 2, 2018

The point of this is to move the .libs folder ahead of "System libraries" in the DLL search path, which it does. However, it appears that this completely removes "Library\bin," from the DLL search path, which leads to "DLL load failed" errors with conda.

@ghost
Copy link
Author

ghost commented Jan 2, 2018

One possible solution could be to only add the directory to the search path if it isn't empty.

@mingwandroid
Copy link
Contributor

Please do not do this. Importing a specific module should not modify the way that process loads subsequent DLLs. The fix for DLL hell for numpy should not break any other modules.

@ghost
Copy link
Author

ghost commented Jan 2, 2018

We already modified PATH so that we could get SciPy out the door. The problem is specifically that if there is an msvcp140.dll in the .libs folder, we want to load that DLL ahead of the system DLL. Do you have any other suggestions for scipy/scipy#8064? It's a complex issue because Python extensions are placed in different folders but the only way to share a DLL between extensions using wheels is to do this, and if we do this, then we want to load our DLLs first.

@ghost
Copy link
Author

ghost commented Jan 2, 2018

IIUC, @cgohlke also does this with his wheels.

@ghost
Copy link
Author

ghost commented Jan 2, 2018

Actually, I'm not correct. I'm leaning toward reverting this and giving up on the linked issue.

@mingwandroid
Copy link
Contributor

mingwandroid commented Jan 2, 2018

if there is an msvcp140.dll in the .libs folder, we want to load that DLL ahead of the system DLL

This goes against the point of the UCRT, the linked issue is that the user has an old version of the UCRT. The UCRT is distributed as part of the OS now, so the correct fix is to run Windows Update AFAICT.

@ghost
Copy link
Author

ghost commented Jan 2, 2018

That's what I initially thought, but the C++ runtime is not included in the UCRT, so Windows update won't fix the problem. The current status of SciPy, which is compiled with the officially supported compiler on the latest official Python distribution (not Anaconda) is that it works perfectly when the user hasn't installed a slighly out-of-date C++ runtime required by some other program, but doesn't work when that's not the case, even if the user has all of their Windows updates. If something goes wrong with the gfortran stuff, that's to be expected because it's a hack. But that's not the part we're talking about here.

@ghost
Copy link
Author

ghost commented Jan 2, 2018

I know this is not really Anaconda's fault, but this issue irritates me because neither Microsoft nor Python has a standardized solution for a problem that occurs when you follow all of their recommendations.

@mingwandroid
Copy link
Contributor

I understand @xoviat, pinging @zooba for his advice.

@insertinterestingnamehere
Copy link
Contributor

I don't see how the new library calls are causing issues with other entries in the dll search path, but couldn't we avoid messing with the search path entirely by using LoadLibrary to load the needed DLLs? Once a DLL with a given name has been loaded by a process, subsequent loads should use the version that has already been loaded, even if that version isn't on the search path.

@mingwandroid
Copy link
Contributor

msvcp140.dll is a core OS/compiler library. This is the the Windows equivalent of loading your own libstdc++ (and forcing it on all other things that use C++).

The potential issues with bundling and forcing this to load are:

  1. You have got on a treadmill of needing to make sure you always provide the latest one and that everyone else's software runs OK when your module got imported first (trusting that others do not depend on bugs in old implementations and that backwards compat. is always perfect).

  2. For people using old versions of numpy their newer software won't work (think C++17 vs a libstdc++ that supports only C++14).

  3. The same, but the other way around, if an older C++-linked extension module gets loaded first then numpy may fail.

  4. It's not clear to me how the kernel32 function SetDefaultDllDirectories modifies the processes DLL search order, but it doesn't seem like something to do lightly (or at all for a language that uses DLLs as an extension mechanism).

@cgohlke
Copy link
Contributor

cgohlke commented Jan 3, 2018

It would help if Microsoft would release or allow to release versioned MSVC runtime packages on PyPI, e.g. msvc14-12.25810-cp35.cp36.cp37-none-win_amd64.whl, that other packages could list as requirements.
Anaconda does this but I am not sure that it is legal to distribute the Microsoft redist DLLs not as part of a "program".
The situation worsened now that CPython 3.6.3+ distutils uses Visual Studio 2017 compilers by default, which comes with updated redistributable DLLs every few months.
CPython distutils tried to address the problem by statically linking the compiler runtime libraries (/MT) to all C extensions. That did not work for other reasons.

@ghost
Copy link
Author

ghost commented Jan 3, 2018

Idea: write a simple "hello world" C++ executable and bundle it in the MSVC wheel. Then it's just a program that happens to be in an archive format that others can use.

@insertinterestingnamehere
Copy link
Contributor

One further complication, it's unclear what the backcompat guarantees are for the MSVC c++ runtime. Supposedly the MSVC 2017 runtime is backwards compatible with binaries built with MSVC 2015 (see https://stackoverflow.com/a/40896092/1935144), but compatibility across versions hasn't been a guarantee with MSVC in the past and it's unclear the extent to which that'll be true in the future.

@cgohlke
Copy link
Contributor

cgohlke commented Jan 3, 2018

write a simple "hello world" C++ executable and bundle it in the MSVC wheel.

I tried that for a short while with bdist_wininst packages but Microsoft might not like that their license is bypassed in an obvious way.

@ghost
Copy link
Author

ghost commented Jan 3, 2018

FTR, I've been thinking about writing an "auditwheel" for windows that would automate much of the packaging of windows wheels. The idea is to use the conda forge to install all of the C libraries and then use numpy.distutils.system_info to grab the include and library directories (which now automatically include conda's include and library directories when it's detected in the PATH) in setup.py. After the package is built, the proposed tool would move all of the C libraries into the .libs folder just like auditwheel (the detection code is already written -- in PyInstaller), but since the rpath mechanism doesn't exist on Windows, the tool would prepend this code to __init__.py in all of the root packages.

It's not an ideal solution, but it's the only sane method I could come up with.

@matthew-brett
Copy link
Contributor

I'm not sure if this is what you mean, but, like auditwheel, you can rename the libraries with unique names. Then use Nathaniel's petool to point the relevant pyd files to the renamed DLLs. That way, there's virtually no risk of a DLL name clash.

@ghost
Copy link
Author

ghost commented Jan 3, 2018

Would that work with msvcp140.dll (I see no reason why it would not)? Then we could just give that a more specific name and rewrite the dependencies.

@mingwandroid
Copy link
Contributor

Then you're back in the multiple different C++ runtimes world of potential hurt. You'd at least lose out on DLLs being loaded multiple times, also I do not know what would happen if msvcp140.dll 's DllMain function gets called multiple times with DLL_PROCESS_ATTACH for slightly different versions of the same library. You could have bad interactions. We need to hear from MS here.

@matthew-brett
Copy link
Contributor

Yes, I guess a renamed msvcp140.dll would work. Then only numpy would be using that runtime, and it should not affect anything else in the process. What DLL is Python.org Python using? Doesn't it have the same problem? I should say that I'm no expert on DLLs ...

@cgohlke
Copy link
Contributor

cgohlke commented Jan 3, 2018

Before messing with msvcp140.dll: has anyone been able to reproduce scipy/scipy#8064? I have not so far. VC140.CRT version 14.0.24210 is the latest version for VS2015 and partially shipped with the official CPython installer. Forcing or suggesting all numpy/scipy users to revert to 14.0.24123 (a RC) is not a good idea IMO.

@zooba
Copy link
Contributor

zooba commented Jan 5, 2018

I'll throw a few dot point comments in. Sorry for not doing nice threading, but I've only just seen this issue (and only because of a direct email - I'm on holiday right now and have been avoiding GitHub notifications like crazy ;) )

  • CPython has no C++ dependencies, so it doesn't include any of the MSVCRT other than vcruntime140.dll (and this is the lowest dependency, so it shouldn't matter if other MSVCRT DLLs are not the same version, though I haven't confirmed this)
  • Using LoadLibrary with a full path to each DLL is a better way to load specific files than messing with the search path. Putting the DLLs alongside your pyd or other DLLs is even easier. Relying on someone else to provide them is best, but at least in the normal PyPI case you can't really do this.
  • If you can hack the PE import table in your pyd, then renaming the CRT DLLs should be okay, though the "correct" way to do this is to use SxS assemblies (and considering we decided maintaining binary compatibility was easier, you probably want to avoid this too)
  • the CRT can be statically linked, so there shouldn't be anything in there that can't handle being loaded multiple times (the issue we hit doing this with vcruntime140 was allocating fibre-local-storage to enable TLS, and there are only ~128 available indices for FLS. I would assume the C++ runtime does TLS using CRT functions, so it shouldn't have to allocate new storage)
  • Binary compatibility between equivalently named DLLs is a guarantee, which means we have a bug - ping me (or python (at) microsoft.com) if you find something like this and we'll make sure it gets to the right team (minimal repros are appreciated, as with any bug report - failing that, crash dumps and pdb files for any pyd or DLLs are invaluable, but if we can rebuild from sources that's probably fine)
  • conda installs provide their own MSVCRT (and I'm working with them to make sure it works), so for those packages you are best to trust that they're correct - same goes for system installed DLLs. In both cases though, you can't force the version that will be used, though you can detect and fail if necessary
  • see this code from CPython for a sample of dumping a DLL version number - if users are regularly hitting issues, it may be worth including something similar so that you can easily get the MSVCRT versions from them (that will also help us if you forward bug reports)
  • IANAL but don't worry about redistributing the CRT - if our lawyers started going after you for whatever reason the engineers would revolt and we'd have VPs calling them off within minutes. (Of course, if someone else sued over a numpy problem caused by the CRT then we'd probably try to avoid directly owning the liability -- successfully, if you've breached the license agreement -- but that's probably a better offer than every "as-is" OSS-licensed dependency you've got)

So I guess the short answer is that you probably can't win this battle - DLL hell is still not a nice place. Statically linking the C++ dependencies is probably going to be the safest option - adding linker options /MT /nodefaultlib:libucrt /nodefaultlib:libvcruntime ucrt vcruntime should do it, though I just typed that from memory and could be wrong - but if there are many DLLs using the runtime then allowing an integrator-provided runtime is going to be more efficient (though the nice thing about static linking for the C++ DLLs is that they aren't all that large anyway, since most of the C++ library is templates and ends up in your own binary anyway).

Building statically for PyPI and dynamically for conda (assuming you're letting Anaconda do the core numpy/scipy builds) is likely the best of both worlds, and then you never have to actually distribute any CRT DLLs. Leave C++ runtime management to the system integrator (Anaconda for conda, the end user for pip) or remove the need for it completely (static linking).

@njsmith
Copy link
Member

njsmith commented Jan 5, 2018

For redistributables like the C++ runtime that don't ship with the OS, I think the only reasonable options are (a) static linking, or (b) mangling the name, using my wacky tool to update the binaries to point to the right name, and then preloading the library with the mangled name using LoadLibrary (or its wrapper, ctypes.DLL) at the top of your package __init__.py. (The latter is basically the same as static linking; the main advantage is that you can write a script like auditwheel that makes this work on arbitrary packages without having to go poking around in one build system after another figuring out how to enable static linking.)

This is assuming that your wheel doesn't have a public C++ ABI that other software outside your wheel might use. This is a very safe assumption for most (all?) Python packages.

@ghost
Copy link
Author

ghost commented Jan 5, 2018

IIUC, if we load msvcp140.dll from the very first MSVC 2015 and then dump that into an extension compiled with MSVC 2017, everything should just work, right? So then this shouldn't be a problem (aside from the effects on conda and/or DLL search path)?

Binary compatibility between equivalently named DLLs is a guarantee, which means we have a bug - ping me (or python (at) microsoft.com) if you find something like this and we'll make sure it gets to the right team (minimal repros are appreciated, as with any bug report - failing that, crash dumps and pdb files for any pyd or DLLs are invaluable, but if we can rebuild from sources that's probably fine)

@isuruf
Copy link
Contributor

isuruf commented Jan 5, 2018

Before messing with msvcp140.dll: has anyone been able to reproduce scipy/scipy#8064? I have not so far. VC140.CRT version 14.0.24210 is the latest version for VS2015 and partially shipped with the official CPython installer. Forcing or suggesting all numpy/scipy users to revert to 14.0.24123 (a RC) is not a good idea IMO.

I can't reproduce it either. Btw, 14.0.24215 is the latest

@cgohlke
Copy link
Contributor

cgohlke commented Jan 5, 2018

Btw, 14.0.24215 is the latest

That's right, the vcredist_*.exe installers shipped with VS2015 community update 3 are 14.0.24215.1 while the individual DLLs (the ones programmers can include next to their exe/DLL) are 14.0.24210.

charris added a commit to charris/numpy that referenced this pull request Jun 29, 2018
Numpy wheels on Windows were clearing the ctypes path when they loaded
the OpenBLAS DLL, leading to failure of DLL loads subsequent to NumPy
import because the needed DLLs could not be found.

This isn't a straight revert, it restores to the 1.15.x version of
the relevant code.

Closes numpy#11431.
charris added a commit that referenced this pull request Jun 29, 2018
BUG: Revert #10229 to fix DLL loads on Windows.
charris added a commit to charris/numpy that referenced this pull request Jul 3, 2018
Numpy wheels on Windows were clearing the ctypes path when they loaded
the OpenBLAS DLL, leading to failure of DLL loads subsequent to NumPy
import because the needed DLLs could not be found.

This isn't a straight revert, it restores to the 1.15.x version of
the relevant code.

Closes numpy#11431.
charris added a commit that referenced this pull request Jul 4, 2018
BUG: Revert #10229 to fix DLL loads on Windows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.