Update extern c ignore (#3025)

* Add a --include-list argument

This allows us to check the exclude list is still valid.

* Add some missing __cplusplus guards

* Update .extern_c_ignore

See #2865

* Add a github workflow to check extern c guards
diff --git a/.extern_c_ignore b/.extern_c_ignore
index 3a7464a..306b1db 100644
--- a/.extern_c_ignore
+++ b/.extern_c_ignore
@@ -25,8 +25,6 @@
 src/common/pico_sync/include/pico/lock_core.h
 src/common/pico_sync/include/pico/sync.h
 src/common/pico_usb_reset_interface_headers/include/pico/usb_reset_interface.h
-src/host/pico_stdio/include/pico/stdio.h
-src/host/pico_time_adapter/include/pico/time_adapter.h
 src/rp2040/pico_platform/include/pico/platform/cpu_regs.h
 src/rp2350/pico_platform/include/pico/platform/cpu_regs.h
 src/rp2_common/pico_bootrom/include/pico/bootrom/lock.h
@@ -36,8 +34,6 @@
 src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_freertos.h
 src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_poll.h
 src/rp2_common/pico_cyw43_arch/include/pico/cyw43_arch/arch_threadsafe_background.h
-src/rp2_common/pico_cyw43_driver/include/pico/btstack_chipset_cyw43.h
-src/rp2_common/pico_fix/rp2040_usb_device_enumeration/include/pico/fix/rp2040_usb_device_enumeration.h
 src/rp2_common/pico_int64_ops/include/pico/int64_ops.h
 src/rp2_common/pico_malloc/include/pico/malloc.h
 src/rp2_common/pico_mem_ops/include/pico/mem_ops.h
@@ -45,9 +41,7 @@
 src/rp2_common/pico_stdio/include/pico/stdio/driver.h
 src/rp2_common/pico_stdio_usb/include/pico/stdio_usb/reset_interface.h
 src/rp2_common/pico_thread_local/include/pico/thread_local.h
-src/rp2_common/pico_time_adapter/include/pico/time_adapter.h
 src/rp2_common/pico_usb_reset/include/pico/usb_reset.h
 src/rp2_common/pico_usb_reset/include/pico/usb_reset_config.h
-src/rp2_common/pico_usb_reset/include/pico/usb_reset_tusb.h
 test/pico_test/include/pico/test.h
 test/pico_test/include/pico/test/xrand.h
diff --git a/.github/workflows/check-extern-c-guards.yml b/.github/workflows/check-extern-c-guards.yml
new file mode 100644
index 0000000..f0a090e
--- /dev/null
+++ b/.github/workflows/check-extern-c-guards.yml
@@ -0,0 +1,39 @@
+name: Check extern C guards
+
+on:
+  push:
+    branches-ignore:
+      - 'iar/**'
+    paths:
+      - '**.h'
+      - '.extern_c_ignore'
+      - 'tools/check_extern_c_guards.py'
+      - '.github/workflows/check-extern-c-guards.yml'
+  pull_request:
+    paths:
+      - '**.h'
+      - '.extern_c_ignore'
+      - 'tools/check_extern_c_guards.py'
+      - '.github/workflows/check-extern-c-guards.yml'
+
+jobs:
+  check-guards:
+    if: github.repository_owner == 'raspberrypi'
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.x'
+
+      - name: Check extern C guards
+        run: |
+          python3 tools/check_extern_c_guards.py \
+            --match "**/include/pico/**" \
+            --ignore-list .extern_c_ignore \
+            .
+
diff --git a/src/host/pico_stdio/include/pico/stdio.h b/src/host/pico_stdio/include/pico/stdio.h
index b2b5807..9d6c60a 100644
--- a/src/host/pico_stdio/include/pico/stdio.h
+++ b/src/host/pico_stdio/include/pico/stdio.h
@@ -6,6 +6,10 @@
 #ifndef _PICO_STDIO_H
 #define _PICO_STDIO_H
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 typedef struct stdio_driver stdio_driver_t;
 
 #define STDIO_ERROR -1
@@ -22,4 +26,8 @@
 #define puts_raw puts
 #define putchar_raw putchar
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/src/host/pico_time_adapter/include/pico/time_adapter.h b/src/host/pico_time_adapter/include/pico/time_adapter.h
index 74f8de2..94a6e14 100644
--- a/src/host/pico_time_adapter/include/pico/time_adapter.h
+++ b/src/host/pico_time_adapter/include/pico/time_adapter.h
@@ -7,6 +7,10 @@
 #ifndef _PICO_TIME_ADAPTER_H
 #define _PICO_TIME_ADAPTER_H
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #ifndef TA_NUM_TIMERS
 #define TA_NUM_TIMERS 1
 #endif
@@ -32,4 +36,8 @@
 alarm_pool_timer_t *ta_timer_instance(uint instance_num);
 alarm_pool_timer_t *ta_default_timer_instance(void);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/src/rp2_common/pico_cyw43_driver/include/pico/btstack_chipset_cyw43.h b/src/rp2_common/pico_cyw43_driver/include/pico/btstack_chipset_cyw43.h
index 037b68d..cf40923 100644
--- a/src/rp2_common/pico_cyw43_driver/include/pico/btstack_chipset_cyw43.h
+++ b/src/rp2_common/pico_cyw43_driver/include/pico/btstack_chipset_cyw43.h
@@ -9,10 +9,18 @@
 
 #include "btstack_chipset.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 /**
  * \brief Return the singleton BTstack chipset CY43 API instance
  * \ingroup pico_btstack
  */
 const btstack_chipset_t * btstack_chipset_cyw43_instance(void);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/include/pico/fix/rp2040_usb_device_enumeration.h b/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/include/pico/fix/rp2040_usb_device_enumeration.h
index 49f115e..62a9dd1 100644
--- a/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/include/pico/fix/rp2040_usb_device_enumeration.h
+++ b/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/include/pico/fix/rp2040_usb_device_enumeration.h
@@ -7,6 +7,10 @@
 #ifndef _PICO_FIX_RP2040_USB_DEVICE_ENUMERATION_H
 #define _PICO_FIX_RP2040_USB_DEVICE_ENUMERATION_H
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 /*! \brief Perform a brute force workaround for USB device enumeration issue
  * \ingroup pico_fix
  *
@@ -14,4 +18,8 @@
  */
 void rp2040_usb_device_enumeration_fix(void);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
\ No newline at end of file
diff --git a/src/rp2_common/pico_time_adapter/include/pico/time_adapter.h b/src/rp2_common/pico_time_adapter/include/pico/time_adapter.h
index 9392fe7..5b898c4 100644
--- a/src/rp2_common/pico_time_adapter/include/pico/time_adapter.h
+++ b/src/rp2_common/pico_time_adapter/include/pico/time_adapter.h
@@ -11,6 +11,10 @@
 #include "hardware/timer.h"
 #include "pico/assert.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #define TA_NUM_TIMERS NUM_GENERIC_TIMERS
 #define TA_NUM_TIMER_ALARMS NUM_ALARMS
 
@@ -102,4 +106,9 @@
 static inline alarm_pool_timer_t *ta_default_timer_instance(void) {
     return PICO_DEFAULT_TIMER_INSTANCE();
 }
+
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/src/rp2_common/pico_usb_reset/include/pico/usb_reset_tusb.h b/src/rp2_common/pico_usb_reset/include/pico/usb_reset_tusb.h
index 1ef73d7..1a980bd 100644
--- a/src/rp2_common/pico_usb_reset/include/pico/usb_reset_tusb.h
+++ b/src/rp2_common/pico_usb_reset/include/pico/usb_reset_tusb.h
@@ -7,6 +7,10 @@
 #ifndef _PICO_USB_RESET_TUSB_H
 #define _PICO_USB_RESET_TUSB_H
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 // Interface descriptor
 #define TUD_RPI_RESET_DESC_LEN  9
 #define TUD_RPI_RESET_DESCRIPTOR(_itfnum, _stridx) \
@@ -61,4 +65,8 @@
     .sof              = NULL
 };
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
\ No newline at end of file
diff --git a/tools/check_extern_c_guards.py b/tools/check_extern_c_guards.py
old mode 100644
new mode 100755
index f463cdf..e6e495f
--- a/tools/check_extern_c_guards.py
+++ b/tools/check_extern_c_guards.py
@@ -32,6 +32,9 @@
     # Skip files listed in an ignore list (reviewed, don't need guards)
     python check_extern_c_guards.py --ignore-list .extern_c_ignore
 
+    # Check only specific files from a list (e.g. to verify your ignore list)
+    python check_extern_c_guards.py /path/to/pico-sdk --include-list .extern_c_ignore
+
     # Exclude specific files or directories
     python check_extern_c_guards.py --exclude "test/**" --exclude "host/**"
 
@@ -114,28 +117,26 @@
     return any(fnmatch.fnmatch(filepath, pat) for pat in matches)
 
 
-def load_ignore_list(filepath: str) -> set[str]:
-    """Load an ignore list file and return a set of normalised paths.
+def load_path_list(filepath: str) -> list[str]:
+    """Load a file containing one path per line.
 
-    The file format is one path per line, relative to the repo / scan root.
-    Blank lines and lines starting with ``#`` are ignored.  Leading and
-    trailing whitespace is stripped, and paths are normalised with forward
-    slashes so that the file works cross-platform.
+    Blank lines and lines starting with ``#`` are treated as comments.
+    Leading/trailing whitespace is stripped and paths are normalised to
+    forward slashes so the file works cross-platform.
     """
-    entries: set[str] = set()
+    entries: list[str] = []
     try:
         with open(filepath, encoding="utf-8") as fh:
             for raw_line in fh:
                 line = raw_line.strip()
                 if not line or line.startswith("#"):
                     continue
-                # Normalise separators and remove trailing slashes
                 normalised = line.replace("\\", "/").rstrip("/")
-                entries.add(normalised)
+                entries.append(normalised)
     except FileNotFoundError:
-        print(f"Warning: ignore list not found: {filepath}", file=sys.stderr)
+        print(f"Warning: path list not found: {filepath}", file=sys.stderr)
     except Exception as exc:
-        print(f"Warning: could not read ignore list {filepath}: {exc}", file=sys.stderr)
+        print(f"Warning: could not read path list {filepath}: {exc}", file=sys.stderr)
     return entries
 
 
@@ -418,6 +419,17 @@
         ),
     )
     parser.add_argument(
+        "--include-list",
+        metavar="FILE",
+        help=(
+            "Path to a file listing headers to check (one per line). "
+            "When given, ONLY these files are checked — the normal directory "
+            "walk or git diff is skipped. Paths are resolved relative to the "
+            "scan root. Useful for re-verifying your ignore list. "
+            "Uses the same file format as --ignore-list (# comments, blanks ok)."
+        ),
+    )
+    parser.add_argument(
         "-v", "--verbose",
         action="store_true",
         help="Show passing files as well",
@@ -434,10 +446,23 @@
 
     excludes = DEFAULT_EXCLUDES + args.exclude
     matches = args.match  # empty list means "match everything"
-    ignore_set = load_ignore_list(args.ignore_list) if args.ignore_list else set()
+    ignore_set = set(load_path_list(args.ignore_list)) if args.ignore_list else set()
 
     # Collect files based on mode
-    if args.staged:
+    if args.include_list:
+        # --include-list: check exactly these files, resolved against root
+        root = os.path.abspath(args.path) if not (args.staged or args.commit or args.range) else os.path.abspath(".")
+        raw_paths = load_path_list(args.include_list)
+        files = []
+        for p in raw_paths:
+            full = os.path.join(root, p)
+            rel = os.path.relpath(full, root)
+            if is_header_file(rel) and not is_excluded(rel, excludes) and is_matched(rel, matches or []):
+                files.append(full)
+        files.sort()
+        label = f"headers from {args.include_list}"
+        rel_root = root
+    elif args.staged:
         files = collect_staged(excludes, matches)
         label = "staged headers"
         rel_root = None