Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 820
bzrr Avatar asked Nov 09 '16 00:11

bzrr


1 Answers

Disclaimer

This answer does not purport to be the correct or best way to embed 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 am unable to test this solution elsewhere, I cannot confirm that it will work in all or even most cases.


How I did it

  1. Create include, lib, lib\python35 and src directories in the project root directory.
  2. Copy all files inside of path\to\python35\include to the include directory in the project root directory.
  3. Zip all files inside of path\to\python35\Lib into a single file called stdlib.zip and put it in the project root directory.¹
  4. Copy all files inside of path\to\python35\DLLs to the lib\python35 directory in the project root directory. The _tkinter.pyd library file should be inside.²
  5. Copy the libpython35.a import library from path\to\python35\libs to the lib directory in the project root directory.
  6. Create a main.py file inside of the src directory in the project root directory 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 directory 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 = (wchar_t**) 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 file in the project root directory 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 directory.

  12. Create and set the TCL_LIBRARY environment variable to "lib\tcl8.6".

Everything should work now.

¹ This is not strictly necessary. You could just as well keep your .py files in a directory and append its path to sys.path.

² The reason why python was raising an ImportError before was because _tkinter.pyd was inside a zip file and thus could not be loaded.

like image 70
bzrr Avatar answered Sep 19 '22 21:09

bzrr