Embed Python 3.5 with tkinter support on Windows
Asked Answered
L

2

7

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?

Laminar answered 9/11, 2016 at 0:6 Comment(0)
L
3

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.

Laminar answered 15/11, 2016 at 11:12 Comment(0)
W
0

Just updating after Jovito answered everything to specify how to make it a light installation (steps 3 to 5 this saves space):

Copy the _tkinter.pyd file mine is in \Pythonpath\DLLs\ into the directory you have the python35.dll located for your embedded installation. You also need 2 DLLs for tkinter to work in the same location tcl86t.dll and tk86t.dll

You need these directories: \Pythonpath\Lib\tkinterand \Pythonpath\tcl\tcl8.6 and \Pythonpath\tcl\tk8.6 and have to set in your main.py script as shown below:

import os
os.environ['TCL_LIBRARY'] = "tcl//tcl8.6"
os.environ['TK_LIBRARY'] = "tcl//tk8.6"

That makes Jovito's answer as lightweight as possible. Use the rest of his answer. Works for me.

Wanitawanneeickel answered 14/11, 2016 at 23:14 Comment(4)
Please, see my answer.Laminar
@Jovito I see in step 8 there appears to be an error when setting the wargv malloc for use in PySys_SetArgv(argc, wargc) - "a value of type "void *" cannot be assigned to an entity of type "wchar_t **" - thanks for your answer BTW this is the last step I have to get it working since Tkinter requires ARGV to be set in the C++.Wanitawanneeickel
The call should be PySys_SetArgv(argc, wargv).Laminar
@Jovito that was a typo I couldn't edit after 5m. The compiler error is "a value of type "void" cannot be assigned to an entity of type "wchar_t **" right at wargv = malloc(argc * sizeof(wchar_t*));Wanitawanneeickel

© 2022 - 2025 — McMap. All rights reserved.