Jovito - 5 months ago 27
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?

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)


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.