Repository: numba/numba
Pull request: Use linkonce_odr linkage for `__excinfo_unwrap_args` global 

Fixes https://github.com/numba/numba/issues/9948

Problem statement:
pycc linking error on Python 3.13 (__excinfo_unwrap_args... multiply defined)
When compiling simple loop from 2 functions using numba.pycc in Python 3.13, I encountered a linking error due to a multiply defined symbol (__excinfo_unwrap_args...). However, the exact same code compiles successfully on Python 3.12.

## Enviroment
```
win-64
python==3.13.2 (Failed) python==3.12 (Works)
llvmlite==0.44.0
numba==0.61.0
numpy==2.1.3
```
## Minimal reproducible example:

```
import numpy as np
from numba.pycc import CC

cc = CC('test_module')

@cc.export('demo', 'f8[:, :](f8[:, :])')
def demo(a):
    rows, cols = a.shape
    b = np.zeros((rows, cols))
    for row in range(rows):
        b[row, :] = np.zeros((1, cols))
    return b

@cc.export('demo2', 'f8[:, :](f8[:, :])')
def demo2(a):
    rows, cols = a.shape
    b = np.zeros((rows, cols))
    for row in range(rows):
        b[row, :] = np.zeros((1, cols))
    return b

if __name__ == "__main__":
    cc.compile()

```
## Stack trace
```
Traceback (most recent call last):
  File "c:\Users\ziling\Desktop\unif\uniformity_utils_src.py", line 141, in <module>
    cc.compile()
    ~~~~~~~~~~^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler_lock.py", line 35, in _acquire_compile_lock
    return func(*args, **kwargs)
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\pycc\cc.py", line 221, in compile
    objects, dll_exports = self._compile_object_files(build_dir)
                           ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler_lock.py", line 35, in _acquire_compile_lock
    return func(*args, **kwargs)
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\pycc\cc.py", line 209, in _compile_object_files
    compiler.write_native_object(temp_obj, wrap=True)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\pycc\compiler.py", line 207, in write_native_object
    library = self._cull_exports()
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler_lock.py", line 35, in _acquire_compile_lock
    return func(*args, **kwargs)
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\pycc\compiler.py", line 159, in _cull_exports
    cres = compile_extra(self.typing_context, self.context,
                        entry.function,
                        entry.signature.args,
                        entry.signature.return_type, flags,
                        locals={}, library=library)
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler.py", line 739, in compile_extra
    return pipeline.compile_extra(func)
           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler.py", line 439, in compile_extra
    return self._compile_bytecode()
           ~~~~~~~~~~~~~~~~~~~~~~^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler.py", line 505, in _compile_bytecode
    return self._compile_core()
           ~~~~~~~~~~~~~~~~~~^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler.py", line 481, in _compile_core
    raise e
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler.py", line 473, in _compile_core
    pm.run(self.state)
    ~~~~~~^^^^^^^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler_machinery.py", line 363, in run
    raise e
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler_machinery.py", line 356, in run
    self._runPass(idx, pass_inst, state)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler_lock.py", line 35, in _acquire_compile_lock
    return func(*args, **kwargs)
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler_machinery.py", line 311, in _runPass
    mutated |= check(pss.run_pass, internal_state)
               ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\compiler_machinery.py", line 272, in check
    mangled = func(compiler_state)
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\typed_passes.py", line 468, in run_pass
    lower.lower()
    ~~~~~~~~~~~^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\lowering.py", line 216, in lower
    self.library.add_ir_module(self.module)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\codegen.py", line 738, in add_ir_module
    self.add_llvm_module(ll_module)
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\numba\core\codegen.py", line 745, in add_llvm_module
    self._final_module.link_in(ll_module)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\llvmlite\binding\module.py", line 174, in link_in
    link_modules(self, other)
    ~~~~~~~~~~~~^^^^^^^^^^^^^
  File "C:\Users\ziling\anaconda3\envs\python3.13\Lib\site-packages\llvmlite\binding\linker.py", line 11, in link_modules
    raise RuntimeError(str(outerr))
RuntimeError: Linking globals named '__excinfo_unwrap_argsd045637d4d71c2c2f78bb75bb1b884b231b0debf': symbol multiply defined!
```

Hints from issue discussion:
I can replicate this error.

Summary:
- This is an internal error.
- The problem is that PYCC is receiving multiple module with public external global symbol `__excinfo_unwrap_argsd045637d4d71c2c2f78bb75bb1b884b231b0debf`. The linkage for such symbols should be changed to allow merging of equivalent definitions.
- The error is independent of Python version. I have replicated on Python 3.12 and 3.13

Commit message: Use linkonce_odr linkage for `__excinfo_unwrap_args` global to prevent error pycc linking error

Patch to review:
diff --git a/numba/core/callconv.py b/numba/core/callconv.py
index 2bf15431467..6d99485f496 100644
--- a/numba/core/callconv.py
+++ b/numba/core/callconv.py
@@ -564,6 +564,8 @@ def emit_unwrap_dynamic_exception_fn(self, module, st_type, nb_types):
 
         fnty = ir.FunctionType(GENERIC_POINTER, [GENERIC_POINTER])
         fn = ir.Function(module, fnty, name)
+        # Linkage is changed to linkonce_odr for PYCC.
+        fn.linkage = "external"
 
         # prevent the function from being inlined
         fn.attributes.add('nounwind')
diff --git a/numba/pycc/compiler.py b/numba/pycc/compiler.py
index a9ad60943fc..9e295576e64 100644
--- a/numba/pycc/compiler.py
+++ b/numba/pycc/compiler.py
@@ -162,6 +162,12 @@ def _cull_exports(self):
                                 entry.signature.return_type, flags,
                                 locals={}, library=library)
 
+            # Fix up dynamic exc globals
+            module = library._final_module
+            for gv in module.functions:
+                if gv.name.startswith("__excinfo_unwrap_args"):
+                    gv.linkage = "linkonce_odr"
+
             func_name = cres.fndesc.llvm_func_name
             llvm_func = cres.library.get_function(func_name)
 
diff --git a/numba/tests/compile_with_pycc.py b/numba/tests/compile_with_pycc.py
index 3f3ae500d3e..8c5b157cc29 100644
--- a/numba/tests/compile_with_pycc.py
+++ b/numba/tests/compile_with_pycc.py
@@ -132,3 +132,16 @@ def internal_str_dict(x):
 @cc_nrt.export('hash_literal_str_A', i8())
 def internal_str_dict():
     return hash("A")
+
+
+# For test dynamic exception
+
+cc_dynexc = CC('pycc_test_dynamic_exc')
+
+@cc_dynexc.export('do_setitem1', 'void(f8[:, :], f8[:, :])')
+def do_setitem1(a, b):
+    a[:, :] = b
+
+@cc_dynexc.export('do_setitem2', 'void(f8[:, :], f8[:, :])')
+def do_setitem2(a, b):
+    a[:, :] = b
diff --git a/numba/tests/test_pycc.py b/numba/tests/test_pycc.py
index 4541b3c10ae..1a9b615cb66 100644
--- a/numba/tests/test_pycc.py
+++ b/numba/tests/test_pycc.py
@@ -262,6 +262,44 @@ def test_c_extension_usecase(self):
             expect = arr * arr
             self.assertPreciseEqual(got, expect)
 
+    def test_dynamic_exc(self):
+        """See https://github.com/numba/numba/issues/9948
+
+        Dynamic exception uses a symbol that in PYCC compilation must be come
+        linkonce_odr linkage to prevent symbol collision.
+        """
+        with self.check_cc_compiled(self._test_module.cc_dynexc) as lib:
+            # these cases do not raise
+            a = np.zeros((2, 2), dtype=np.float64)
+            b = np.ones((2, 2), dtype=np.float64)
+            lib.do_setitem1(a, b)
+            self.assertPreciseEqual(a, b)
+
+            a = np.zeros((2, 2), dtype=np.float64)
+            lib.do_setitem2(a, b)
+            self.assertPreciseEqual(a, b)
+
+            # these cases will raise a dynamic exc
+            a = np.zeros((2, 2), dtype=np.float64)
+            b = np.ones((2, 3), dtype=np.float64)
+            with self.assertRaises(ValueError) as raises:
+                lib.do_setitem1(a, b)
+
+            self.assertIn(
+                f"cannot assign slice of shape {b.shape} from "
+                f"input of shape {a.shape}",
+                str(raises.exception))
+
+            a = np.zeros((4, 6), dtype=np.float64)
+            b = np.ones((4, 5), dtype=np.float64)
+            with self.assertRaises(ValueError) as raises:
+                lib.do_setitem2(a, b)
+
+            self.assertIn(
+                f"cannot assign slice of shape {b.shape} from "
+                f"input of shape {a.shape}",
+                str(raises.exception))
+
 
 @needs_setuptools
 @skip_if_py313_on_windows


Code review comments:
diff_hunk: @@ -262,6 +262,44 @@ def test_c_extension_usecase(self):
             expect = arr * arr
             self.assertPreciseEqual(got, expect)
 
+    def test_dynamic_exc(self):
+        """See https://github.com/numba/numba/issues/9948
+
+        Dynamic exception uses a symbol that in PYCC compilation must be come
path: numba/tests/test_pycc.py
text: @user1:
```suggestion
        Dynamic exception uses a symbol that in PYCC compilation must become
```

diff_hunk: @@ -564,6 +564,8 @@ def emit_unwrap_dynamic_exception_fn(self, module, st_type, nb_types):
 
         fnty = ir.FunctionType(GENERIC_POINTER, [GENERIC_POINTER])
         fn = ir.Function(module, fnty, name)
+        # Linkage is changed to linkonce_odr for PYCC.
path: numba/core/callconv.py
text: @user1:
```suggestion
        # Linkage is changed to linkonce_odr for PYCC. External is the default.
```