Repository: matplotlib/matplotlib
Pull request: Use both TCL_SETVAR and TCL_SETVAR2 for tcl 9 support
Provides both the older and newer APIs for compatibility with both tcl 9 plus and older versions.

Co-authored-by: Antony Lee <anntzer.lee@gmail.com>


Closes #29126 
<!--
Thank you so much for your PR!  To help us review your contribution, please check
out the development guide https://matplotlib.org/devdocs/devel/index.html
-->

## PR summary
<!-- Please describe the pull request, using the questions below as guidance, and link to any relevant issues and PRs:

- Why is this change necessary?
- What problem does it solve?
- What is the reasoning for this implementation?

Additionally, please summarize the changes in the title, for example "Raise ValueError on
non-numeric input to set_xlim" and avoid non-descriptive titles such as "Addresses
issue #8576".
-->


## PR checklist
<!-- Please mark any checkboxes that do not apply to this PR as [N/A].-->

- [x] "closes #0000" is in the body of the PR description to [link the related issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
- [x] new and changed code is [tested](https://matplotlib.org/devdocs/devel/testing.html)
- [x] *Plotting related* features are demonstrated in an [example](https://matplotlib.org/devdocs/devel/document.html#write-examples-and-tutorials)
- [x] *New Features* and *API Changes* are noted with a [directive and release note](https://matplotlib.org/devdocs/devel/api_changes.html#announce-changes-deprecations-and-new-features)
- [x] Documentation complies with [general](https://matplotlib.org/devdocs/devel/document.html#write-rest-pages) and [docstring](https://matplotlib.org/devdocs/devel/document.html#write-docstrings) guidelines

<!--We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.-->


Problem statement:
[Bug]: TkAgg backend is broken with tcl/tk 9.0
### Bug summary

When trying to use TkAgg backend with tkinter compiled with tcl/tk 9.0(ex. `python-tk` with Python 3.13 from Homebrew), the backend fails to load

### Code for reproduction

```Python
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
```


### Actual outcome

```
    from . import _tkagg
ImportError: Failed to load Tcl_SetVar
```

### Expected outcome

works as expected

### Additional information

`Tcl_SetVar` function is removed in tcl 9.0, which is used in `DpiSubclassProc` of `src/_tkagg.cpp` on Windows. The function can easily be replaced with `Tcl_SetVar2` if exists.

### Operating system

macOS 14.6

### Matplotlib Version

3.9.2

### Matplotlib Backend

_No response_

### Python version

3.13

### Jupyter version

_No response_

### Installation

pip

Hints from issue discussion:
it looks like tcl/tk 9.0 was release on Sept 26, 2024 https://www.tcl.tk/software/tcltk/9.0.html 

Looking at https://www.tcl.tk/man/tcl8.6/TclLib/SetVar.htm I do not see any warning that `Tcl_SetVar` is deprecated and it still appears to be documented in https://www.tcl.tk/man/tcl9.0/TclLib/SetVar.html .  Can you please point us at any documentation of the removal?

I instead suspect that something about 

https://github.com/matplotlib/matplotlib/blob/ca39d41a38f6530b087ed46fee8476e15ea69737/src/_tkagg.cpp#L241-L256

is not playing well with tk9.0 instead.  However, given that this appears to only be seen with brew it is not clear to me yet if this is a "incompatible with tcl/tk 9.0" or "incompatible with brew's build of tcl/tk 9.0".  The first is something we should fix, the second is something that should be reported to brew first.
@tacaswell Looking further into tcl/tk changes, I can find that the function definition(symbol) is removed, which was in `generic/tclVar.c` of the tcl source tree.
The function still remains as public API because it's defined as a macro in `generic/tclDecls.h`, though.
https://github.com/tcltk/tcl/blob/f152ab6f3f207fe3491290cd83359dc399e188a0/generic/tclDecls.h#L3942-L3943
Ah, so from a c-extension point of view it is not a breaking change, but because we are doing something a bit funny (which we do to drop the requirement for a build-time dependency on tk) we are broken.

I think @richardsheridan and @anntzer should weigh in on how to fix this, but I suspect that we need to adapt to find one or the other and then support calling either where we use it.
Seems reasonable enough, probably something along the lines of (totally untested)
```patch
diff --git i/src/_tkagg.cpp w/src/_tkagg.cpp
index 04ca3d4d1b..15b795d688 100644
--- i/src/_tkagg.cpp
+++ w/src/_tkagg.cpp
@@ -92,6 +92,7 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK;
 // Global vars for Tcl functions.  We load these symbols from the tkinter
 // extension module or loaded Tcl libraries at run-time.
 static Tcl_SetVar_t TCL_SETVAR;
+static Tcl_SetVar2_t TCL_SETVAR2;
 
 static void
 mpl_tk_blit(py::object interp_obj, const char *photo_name,
@@ -173,7 +174,13 @@ DpiSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
             std::string dpi = std::to_string(LOWORD(wParam));
 
             Tcl_Interp* interp = (Tcl_Interp*)dwRefData;
-            TCL_SETVAR(interp, var_name.c_str(), dpi.c_str(), 0);
+            if (TCL_SETVAR) {
+                TCL_SETVAR(interp, var_name.c_str(), dpi.c_str(), 0);
+            } else if (TCL_SETVAR2) {
+                TCL_SETVAR2(interp, var_name.c_str(), NULL, dpi.c_str(), 0);
+            } else {
+                // Perhaps add a failing assertion here.
+            }
         }
         return 0;
     case WM_NCDESTROY:
@@ -246,13 +253,16 @@ bool load_tcl_tk(T lib)
     if (auto ptr = dlsym(lib, "Tcl_SetVar")) {
         TCL_SETVAR = (Tcl_SetVar_t)ptr;
     }
+    if (auto ptr = dlsym(lib, "Tcl_SetVar2")) {
+        TCL_SETVAR2 = (Tcl_SetVar2_t)ptr;
+    }
     if (auto ptr = dlsym(lib, "Tk_FindPhoto")) {
         TK_FIND_PHOTO = (Tk_FindPhoto_t)ptr;
     }
     if (auto ptr = dlsym(lib, "Tk_PhotoPutBlock")) {
         TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)ptr;
     }
-    return TCL_SETVAR && TK_FIND_PHOTO && TK_PHOTO_PUT_BLOCK;
+    return (TCL_SETVAR || TCL_SETVAR2) && TK_FIND_PHOTO && TK_PHOTO_PUT_BLOCK;
 }
 
 #ifdef WIN32_DLL
@@ -343,8 +353,8 @@ PYBIND11_MODULE(_tkagg, m, py::mod_gil_not_used())
         throw py::error_already_set();
     }
 
-    if (!TCL_SETVAR) {
-        throw py::import_error("Failed to load Tcl_SetVar");
+    if (!(TCL_SETVAR || TCL_SETVAR2)) {
+        throw py::import_error("Failed to load Tcl_SetVar and Tcl_SetVar2");
     } else if (!TK_FIND_PHOTO) {
         throw py::import_error("Failed to load Tk_FindPhoto");
     } else if (!TK_PHOTO_PUT_BLOCK) {
diff --git i/src/_tkmini.h w/src/_tkmini.h
index 85f245815e..1c74cf9720 100644
--- i/src/_tkmini.h
+++ w/src/_tkmini.h
@@ -104,6 +104,9 @@ typedef int (*Tk_PhotoPutBlock_t) (Tcl_Interp *interp, Tk_PhotoHandle handle,
 /* Tcl_SetVar typedef */
 typedef const char *(*Tcl_SetVar_t)(Tcl_Interp *interp, const char *varName,
                                     const char *newValue, int flags);
+/* Tcl_SetVar2 typedef */
+typedef const char *(*Tcl_SetVar2_t)(Tcl_Interp *interp, const char *part1, const char *part2,
+                                     const char *newValue, int flags);
 
 #ifdef __cplusplus
 }
```
?
Have confirmed that the patch above works, PR incoming.

Commit message: Use both TCL_SETVAR and TCL_SETVAR2 for tcl 9 support

Co-authored-by: Antony Lee <anntzer.lee@gmail.com>

Patch to review:
diff --git a/src/_tkagg.cpp b/src/_tkagg.cpp
index 04ca3d4d1b84..1afe19ab417f 100644
--- a/src/_tkagg.cpp
+++ b/src/_tkagg.cpp
@@ -92,6 +92,7 @@ static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK;
 // Global vars for Tcl functions.  We load these symbols from the tkinter
 // extension module or loaded Tcl libraries at run-time.
 static Tcl_SetVar_t TCL_SETVAR;
+static Tcl_SetVar2_t TCL_SETVAR2;
 
 static void
 mpl_tk_blit(py::object interp_obj, const char *photo_name,
@@ -173,7 +174,15 @@ DpiSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
             std::string dpi = std::to_string(LOWORD(wParam));
 
             Tcl_Interp* interp = (Tcl_Interp*)dwRefData;
-            TCL_SETVAR(interp, var_name.c_str(), dpi.c_str(), 0);
+            if (TCL_SETVAR) {
+                TCL_SETVAR(interp, var_name.c_str(), dpi.c_str(), 0);
+            } else if (TCL_SETVAR2) {
+                TCL_SETVAR2(interp, var_name.c_str(), NULL, dpi.c_str(), 0);
+            } else {
+                // This should be prevented at import time, and therefore unreachable.
+                // But defensively throw just in case.
+                throw std::runtime_error("Unable to call Tcl_SetVar or Tcl_SetVar2");
+            }
         }
         return 0;
     case WM_NCDESTROY:
@@ -246,13 +255,16 @@ bool load_tcl_tk(T lib)
     if (auto ptr = dlsym(lib, "Tcl_SetVar")) {
         TCL_SETVAR = (Tcl_SetVar_t)ptr;
     }
+    if (auto ptr = dlsym(lib, "Tcl_SetVar2")) {
+        TCL_SETVAR2 = (Tcl_SetVar2_t)ptr;
+    }
     if (auto ptr = dlsym(lib, "Tk_FindPhoto")) {
         TK_FIND_PHOTO = (Tk_FindPhoto_t)ptr;
     }
     if (auto ptr = dlsym(lib, "Tk_PhotoPutBlock")) {
         TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)ptr;
     }
-    return TCL_SETVAR && TK_FIND_PHOTO && TK_PHOTO_PUT_BLOCK;
+    return (TCL_SETVAR || TCL_SETVAR2) && TK_FIND_PHOTO && TK_PHOTO_PUT_BLOCK;
 }
 
 #ifdef WIN32_DLL
@@ -343,8 +355,8 @@ PYBIND11_MODULE(_tkagg, m, py::mod_gil_not_used())
         throw py::error_already_set();
     }
 
-    if (!TCL_SETVAR) {
-        throw py::import_error("Failed to load Tcl_SetVar");
+    if (!(TCL_SETVAR || TCL_SETVAR2)) {
+        throw py::import_error("Failed to load Tcl_SetVar and Tcl_SetVar2");
     } else if (!TK_FIND_PHOTO) {
         throw py::import_error("Failed to load Tk_FindPhoto");
     } else if (!TK_PHOTO_PUT_BLOCK) {
diff --git a/src/_tkmini.h b/src/_tkmini.h
index 85f245815e4c..1c74cf9720f8 100644
--- a/src/_tkmini.h
+++ b/src/_tkmini.h
@@ -104,6 +104,9 @@ typedef int (*Tk_PhotoPutBlock_t) (Tcl_Interp *interp, Tk_PhotoHandle handle,
 /* Tcl_SetVar typedef */
 typedef const char *(*Tcl_SetVar_t)(Tcl_Interp *interp, const char *varName,
                                     const char *newValue, int flags);
+/* Tcl_SetVar2 typedef */
+typedef const char *(*Tcl_SetVar2_t)(Tcl_Interp *interp, const char *part1, const char *part2,
+                                     const char *newValue, int flags);
 
 #ifdef __cplusplus
 }


Code review comments:
diff_hunk: @@ -343,8 +355,8 @@ PYBIND11_MODULE(_tkagg, m, py::mod_gil_not_used())
         throw py::error_already_set();
     }
 
-    if (!TCL_SETVAR) {
-        throw py::import_error("Failed to load Tcl_SetVar");
+    if (!(TCL_SETVAR || TCL_SETVAR2)) {
+        throw py::import_error("Failed to load Tcl_SetVar and Tcl_SetVar2");
path: src/_tkagg.cpp
text: @user1:
```suggestion
        throw py::import_error("Failed to load Tcl_SetVar or Tcl_SetVar2");
```