Jovito Jovito - 19 days ago 5
Python Question

How to embed Python 3.5 with tkinter support on Windows

My project structure looks like this:

emb
| CMakeLists.txt
| main.c
| python35.lib
| stdlib.zip
| _tkinter.pyd
|
+---include
| |
| | abstract.h
| | accu.h
| | asdl.h
...
| | warnings.h
| | weakrefobject.h
|
+---build
| | emb.exe


stdlib.zip contains the DLLs, Lib and site-packages directories from Python 3.5.2 installation whose paths are appended to
sys.path
. I'm implicitly loading python35.dll by linking to python35.lib which contains the stubs for all of the exported functions in the DLL. Here's the contents of CMakeLists.txt:

cmake_minimum_required(VERSION 3.6)
project(embpython)

set(SOURCE_FILES main.c)
add_executable(${PROJECT_NAME} ${SOURCE_FILES})

set(PYTHON_INCLUDE_DIR include)
include_directories(${PYTHON_INCLUDE_DIR})

target_link_libraries(
${PROJECT_NAME}
${CMAKE_CURRENT_LIST_DIR}/python35.lib
${CMAKE_CURRENT_LIST_DIR}/_tkinter.pyd)


And here's the contents of main.c:

#include <Python.h>

int main(int argc, char** argv)
{
wchar_t* program_name;
wchar_t* sys_path;
char* path;

program_name = Py_DecodeLocale(argv[0], NULL);
if (program_name == NULL)
{
fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
exit(1);
}
Py_SetProgramName(program_name);

path = "stdlib.zip;stdlib.zip/DLLs;stdlib.zip/Lib;"
"stdlib.zip/site-packages";
sys_path = Py_DecodeLocale(path, NULL);
Py_SetPath(sys_path);

Py_Initialize();

PySys_SetArgv(argc, argv);

PyRun_SimpleString("import tkinter\n");

Py_Finalize();
PyMem_RawFree(sys_path);
PyMem_RawFree(program_name);
return 0;
}


Now, here's the error I'm getting:

Traceback (most recent call last):
File "<string>", line 1, in <module>
File " ... emb\stdlib.zip\Lib\tkinter\__init__.py", line 35, in <module>
ImportError: DLL load failed: The specified module could not be found.


What am I doing wrong and how can I fix it?

Answer

This answer does not purport to be the best solution to the problem of embedding Python 3.5 with Tkinter support. The step-by-step format only reflects the fact that this was how I managed to get everything working on my machine, and since I was unable to test this solution elsewhere, I cannot attest to its efficacy. Nonetheless, If you're as desperate as I was, read on.


  1. Create include, lib, lib\python35 and src directories in the project root.
  2. Copy all files inside of C:\path\to\python35\DLLs to the lib\python35 directory in the project root.
  3. Copy all files inside of C:\path\to\python35\include to the include directory in the project root.
  4. Copy the libpython35.a import library from C:\path\to\python35\libs to the lib directory in the project root.
  5. Zip all files inside of C:\path\to\python35\Lib into a single file called stdlib.zip and put it in the project root directory. (This is not strictly necessary. You could just as well keep your .py files in a directory and append its path to sys.path).
  6. Create a main.py file inside of the src directory in the project root with the following contents:

    import tkinter as tk
    
    def run():
        root = tk.Tk()
        root.mainloop()
    
  7. Zip main.py into a single file called source.zip and put it in the project root directory.
  8. Create a main.c file inside of the src directory in the project root with the following contents:

    // WARNING: I did not check for errors but you definitely should!
    #import <Python.h>
    
    static const char* SYS_PATH = "source.zip;stdlib.zip;lib/python35";
    
    int main(int argc, char** argv)
    {
        wchar_t* program = NULL;
        wchar_t** wargv = NULL;
        wchar_t* sys_path = NULL;
        int i;
    
        program = Py_DecodeLocale(argv[0], NULL);
        Py_SetProgramName(program);
    
        sys_path = Py_DecodeLocale(SYS_PATH, NULL);
        Py_SetPath(sys_path);
    
        Py_Initialize();
    
        wargv = malloc(argc * sizeof(wchar_t*));
        for (i = 0; i < argc; i++)
            wargv[i] = Py_DecodeLocale(argv[i], NULL);
        PySys_SetArgv(argc, wargv);
    
        PyRun_SimpleString("import main\n"
                           "main.run()\n");
    
        Py_Finalize();
        PyMem_RawFree(program);
        PyMem_RawFree(sys_path);
        for (i = 0; i < argc; i++)
            PyMem_RawFree(wargv[i]);
        free(wargv);
    
        return 0;
    }
    
  9. Create a CMakeLists.txt in the project root with the following contents:

    cmake_minimum_required(VERSION 3.6)
    project(emb)
    
    set(SOURCE_FILES src/main.c)
    add_executable(emb ${SOURCE_FILES})
    
    include_directories(include)
    
    add_library(libpython35 STATIC IMPORTED)
    set_property(
        TARGET libpython35 PROPERTY IMPORTED_LOCATION
        ${CMAKE_CURRENT_LIST_DIR}/lib/libpython35.a)
    
    target_link_libraries(emb libpython35)
    
  10. Build and run. If you did everything correctly up to this point, you should see something like this:

    Traceback (most recent call last):
      File "<string>", line 2, in <module>
      File "C:\path\to\project\stdlib.zip\tkinter\__init__.py", line 1868, in __init__
    _tkinter.TclError: Can't find a usable init.tcl in the following directories:
        C:/path/to/project/lib/lib/tcl8.6
        C:/path/to/project/lib/tcl8.6 
        C:/path/to/project/library
        C:/path/to/project/tcl8.6.4/library
    

    Tcl and Tk directories are nowhere to be found. We need to bring those in and update the TCL_LIBRARY enviroment variable.

  11. Copy tcl8.6 and tk8.6 directories from C:\path\to\python35\tcl to the lib directory in the project root.

  12. Add the following line to main.py before doing anything in tkinter:

    os.environ['TCL_LIBRARY'] = "lib/tcl8.6"
    

Everything should work now.

Comments