Nim, as far as I known, uses GC, but can use C headers to generate bindings. C and C++ doesn't use GC, so there could be some weird situations, when C/C++ library free an memory and our program store pointer to this memory somewhere.
Few years ago I had similar problem and solve it. I use free wrapper, iterate through list stores real memory pointers and replace pointer in this list to 0, where it was matched. When GC ran, it iterate on this list and free elements with NULL pointer.
Solution for Nim could been similar. Just add new pointer type. It will be wrapper of normal pointer. To create my pointer type, programmer might specify free function and it will be injected or something at runtime (program start). When program tries to assign normal pointer to my pointer type, we iterate on wrappers list and check it exist currently. If not, we wrap it and create a new list element. When assigning my pointer type var to variable of the same type, we copy address to a wrapper.
When program uses my pointer type var, we check there exist NULL value. If it's true we will throw an exception.
We already have this in Nim.
We have ptr in Nim. This is a raw C pointer. The GC does not track it. ref is a GC tracked pointer.
you can use gc_ref and gc_unref to convert between the two freely: https://nim-lang.org/docs/system.html#GC_ref%2Cref.T
Here is an example of working with the C FFI using cstrings from the manual: https://nim-lang.org/docs/manual.html#types-cstring-type
It works similarly for other types.