I'm experiencing function pointer compatibility issues after upgrading from Nim 2.0.6 to 2.2.4 with my Tcl bindings. I'd like to understand what changed in how Nim generates C code for function pointer types.
Here's a minimal case with my type definition:
type
PInterp = ptr TInterp
TInterp{.final.} = object
PObj = ptr TObj
TObj{.final.} = object
TGetBooleanFromObj = proc(interp: PInterp, objPtr: PObj, boolPtr: var cint): cint {.cdecl.}
TclStubs = object
tcl_GetBooleanFromObj : TGetBooleanFromObj
var tclStubsPtr* {.importc: "tclStubsPtr", header: "tclDecls.h".} : ptr TclStubs
var GetBooleanFromObj = cast[TGetBooleanFromObj](tclStubsPtr.tcl_GetBooleanFromObj)
The error in Nim 2.2.4 :
error: incompatible function pointer types assigning to 'tyProc__c9cm9bsZNNcAzphBg2GX5BuQ'
(aka 'int (*)(struct tyObject_TInterp__*, struct tyObject_TObj__*, int *)')
from 'int (*const)(Tcl_Interp *, Tcl_Obj *, int *)'
[-Wincompatible-function-pointer-types]
What Nim generates: Based on the error, Nim 2.2.4 generates:
int (*)(struct tyObject_TInterp__*, struct tyObject_TObj__*, int *)
What C expects: The Tcl library provides:
int (*const)(Tcl_Interp *, Tcl_Obj *, int *)
The only difference I see is the const qualifier on the function pointer itself.
What's the recommended approach for handling const function pointers from C libraries?
Use {.passC: "-Wno-incompatible-function-pointer-types".} ? Redefine all my proc types somehow ?
Environment:
Thanks
The only difference I see is the const qualifier on the function pointer itself.
A more significant issue is that you're using the wrong struct types.
What happens if you mark TInterp with .importc: "Tcl_Interp", and TObj with .importc: "Tcl_Obj"?
Great suggestion! Using .importc fixed the struct name issues. Now I have:
BEFORE: int (*)(struct tyObject_TInterp__*, ...)
NOW: int (*)(struct Tcl_Interp *, ...)
The struct names match perfectly now! However, I still have the const qualifiers issue:
So the .importc approach solved the major structural issue, but the const problem remains.
What's the best way to handle the const qualifiers?
Adding .header: tclDecls.h causes macro definition errors (EXTERN, CONST84_RETURN, etc. are undefined).
Interestingly, I found that:
On const char *: this works for me.
Yes, using the constChar approach from the article works, but I need to do constChar("abc") everywhere. For 19+ functions, this adds significant verbosity, but thank you for the solution.
For const void * I guess you'd use pointer instead of cstring, but I haven't tried.
That's what I do - I use a simple pointer for void*, but the core issue remains: even with perfect parameter types, I still get the function pointer const qualifier error:
int (*)(const char *, int) // What Nim generates
int (*const)(const char *, int) // What Tcl provides
The difference is the const on the function pointer itself, not the parameters. This affects all my Tcl stub functions.
macro definition errors
Right, it's supposed to be tcl.h...
Yes, using the constChar approach from the article works, but I need to do constChar("abc") everywhere.
Well you can always do something like
converter toCstringConst*(s: cstring): cstringConst {.inline.} =
return cstringConst(s)
I still get the function pointer const qualifier error:
Do you have a minimal reproducible example?
(To me it looks suspicious, I think that would mean you're reassigning a const pointer somehow and that seems unrelated to FFI?)
target the DLL/lib*.so
you can use a static library / {.compile.} a C file without using {.header.} as well, there's no difference vs DLL - the point of using the C header is to act as a check on whether your bindings are correct, whether nim codegen is buggy / generates an incompatible ABI and to get the benefit of extra annotations on the C side through compiler-specific extensions and annotations (which allow better optimizations).
I tried to reproduce an example with the problems I encountered with version 2.2.4.
# Minimal reproduction case
{.passC:"-DUSE_TCL_STUBS"}
{.passC:"-I/usr/local/Cellar/tcl-tk/9.0.1/include/tcl-tk"}
{.passL:"-L/usr/local/Cellar/tcl-tk/9.0.1/lib/"}
{.passL:"-ltclstub"}
# nim c --cc:gcc --app:lib -d:release --out:test.dylib test.nim
type
PInterp* = ptr TInterp
TInterp{.final, importc: "Tcl_Interp", header: "tcl.h".} = object
type
TPkgProvideEx = proc(interp: PInterp, name: cstring, version: cstring,
clientData: pointer): cint {.cdecl.}
type
TclStubs = object
tcl_PkgProvideEx: TPkgProvideEx
# I have a diffence here with real code, header is not tcl.h but tclDecls.h in my case.
# I have not been able to resolve this issue.
var tclStubsPtr{.importc: "tclStubsPtr", header: "tcl.h".} : ptr TclStubs
var PkgProvideEx = cast[TPkgProvideEx](tclStubsPtr.tcl_PkgProvideEx)
# error: incompatible function pointer types assigning to 'tyProc__Fis3I1uC8G9brjiFef1652w'
# (aka 'int (*)(struct Tcl_Interp *, char *, char *, void *)') from 'int (*const)(Tcl_Interp *, const char *, const char *, const void *)'
# (aka 'int (*const)(struct Tcl_Interp *, const char *, const char *, const void *)') [-Wincompatible-function-pointer-types]
# 74 | PkgProvideEx__test_u13 = (*tclStubsPtr).tcl_PkgProvideEx;
# | ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Basically, I defined it as follows:
type
PObj* = ptr TObj
PPObj* = ptr UncheckedArray[PObj]
TObj* = object
refCount*: int
But I'm having trouble again with the qualifier const*
I don't know if @nrk's solution would work in this case, on an object.
Thanks for your assistance.
You're too focused on const. A Tcl_Obj *const is a constant pointer to a non-constant object; unless you're doing something extremely wrong the compiler won't complain about that.
The actual issue is that you aren't marking your object with .importc:
# untested, but I think it should work
type
TObj* {.importc: "Tcl_Object", header: "<tcl.h>", .incompleteStruct.} = object
refCount: cint # beware, a cint is *not* the same as int.
# blah...
You either have to import all C functions and types properly, or go with the dynlib route as suggested by Araq. (But with dynlibs you have to get the ABI right on your own, e.g. your incorrect definition of refCount still wouldn't fly but the compiler won't tell you.)
You're too focused on const
Yes I know, but since I've had problems here from the start, I'm focusing on this error.
type
# Fixed the wrongly placed dot here.
TObj* {.importc: "Tcl_Object", header: "tcl.h", incompleteStruct.} = object
refCount: cint
PObj* = ptr TObj
PPObj* = ptr UncheckedArray[PObj]
My C function :
EXTERN Tcl_Obj * Tcl_NewListObj(int objc, Tcl_Obj *const objv[]);
Declaration :
type
TNewListObj = proc(objc: cint, objv: PPObj): PObj {.cdecl.}
type TclStubs = object
tcl_NewListObj : TNewListObj
var tclStubsPtr* {.importc: "tclStubsPtr", header: "tclDecls.h".} : ptr TclStubs
var NewListObj = tclStubsPtr.tcl_NewListObj
And my error :
error: incompatible function pointer types assigning to 'tyProc__EcJcCoApc9bd4h3jZs4XAog' (aka 'struct Tcl_Obj *(*)(int, struct Tcl_Obj **)') from 'Tcl_Obj *(*const)(int, Tcl_Obj *const *)' (aka 'struct Tcl_Obj *(*const)(int, struct Tcl_Obj *const *)') [-Wincompatible-function-pointer-types]
123 | NewListObj__bindingsZtclZbinding_u218 = (*tclStubsPtr).tcl_NewListObj;
Nim generates this for me:
struct Tcl_Obj* (*)(
int, // objc
struct Tcl_Obj** // objv
)
What Tcl expects:
struct Tcl_Obj* (*const)(
int,
Tcl_Obj* const*
)
I had seen @Araq's solution , but if I understand pragma dynlib correctly, it only works with shared libraries. In my case, I use a static library. But I would like to keep the types correctly defined.
Thanks
I had seen @Araq's solution , but if I understand pragma dynlib correctly, it only works with shared libraries.
Good point but you can still write compatible Nim types (that ignore C's const) and link statically to the TCL codebase. For this you use importc on the procs but not on the types! And you don't use dynlib.
Ah so you wan't a Tcl_Obj *const *. Well I don't know how to do that cleanly, but in the case you mention you can just do
let NewListObj {.importc: "tclStubsPtr->tcl_NewListObj", header: "<tcl.h>".}: TNewListObj
but that won't work if you actually want to store the function... I guess for that an easy way is to emit a typedef and then importc that as the function:
{.emit: """
typedef Tcl_Obj *(*my_TNewListObj)(int objc, Tcl_Obj *const *);
""".}
type
TNewListObj {.importc: "my_TNewListObj".} =
proc(objc: cint, objv: PPObj): PObj {.cdecl.}
Ah so you wan't a Tcl_Obj *const *.
@nrk , yes, yes I want Tcl_Obj* const*, sorry if I wasn't clear enough or I didn't write the right thing, but even with your definition of the Tcl_Obj type, the compiler generates an error for me.
@Araq, sorry, I don't understand what you're trying to tell me. Can I do without const in my definitions ? Do I understand correctly ?
I haven't abandoned the idea of achieving my binding ! I think I understand what you were trying to tell me by not defining imports on types.
My question is this: should I define all types without importc or only those that cause me problems ? Is it recommended to mix types with and without importc ?