Do you think this could be a viable approach for passing a closure to a C++ member that could be called by C++ ?
As my knowledge is still limited, i'm not sure this is safe to do or i could lead into shooting myself in the foot. Any advice would be great !
{.experimental: "callOperator".}
{.emit: """/*TYPESECTION*/
#include <functional>
std::function<void()> bindClosure(void (*functionPointer)(void*), void* functionEnvironment)
{
return std::bind(functionPointer, functionEnvironment);
}
""".};
type
VoidUnaryFunction {.importcpp: "std::function<void()>", header: "<functional>".} = object
Closure = proc (env: pointer) {.nimcall.}
proc toRawProc(f: proc()): Closure = cast[Closure](rawProc f)
proc bindClosureInternal(p: Closure, env: pointer): VoidUnaryFunction {.importcpp: "bindClosure(@)".}
proc bindClosure(f: proc()): VoidUnaryFunction = bindClosureInternal(toRawProc f, rawEnv f)
proc `()`(f: var VoidUnaryFunction) {.importcpp: "std::invoke(@)", header: "<functional>".}
# Testing
proc makeClosure(arg: int): proc () =
let s = "from closure scope "
return (proc () = echo s & $arg)
var y = bindClosure(makeClosure(42))
y()
What is the lifetime of the closure scope that rawEnv points to?
Can a closure have parameters? How do i specify them when i decompose the closure into his rawProc and rawEnv counterparts?
I don't really know what std::function is
It's a polymorphic function wrapper that is able to store function pointers, pointer to member function, capturing lambdas, bind result and so on. See https://en.cppreference.com/w/cpp/utility/functional/function for more info.
You can check out answers for these questions yourself if you compile with something like nim c -d:danger --nimcache:ccode file.nim and then check the C files in the ccode folder.
For example, for:
proc main =
var external: string
proc my(x: int) =
echo x
echo external
my(5)
main()
Relevant parts in the C code:
# The environment of the closure
struct tyObject_Env_testdotnim_main___s9aFarTYu8wC9aBkHyu9aWVqQ {
RootObj Sup;
NI colonstate_;
NimStringDesc* external1;
};
# The closure struct itself that has two fields: the pointer to the closure itself and pointer to the environment
# Also, the function signature is all the normal arguments of the closure, and then the closure environment
typedef struct {
N_NIMCALL_PTR(void, ClP_0) (NI x, void* ClE_0);
void* ClE_0;
} tyProc__SqRJqa5aOiKB4EuKlKh9asA;
Thanks! yeah i did exactly that and noticed the closure function pointer in C is written as returnType (*)(Args... args, void* env) with additional parameters prepended before the env pointer.
Is there a way to attach a nim destructor to an imported cpp object that will both call the C++ destructor and GC_unref the closure ?
Just passing a nimcall procedure with an object type seems simpler and don't need to care about how to manage heap. But object types that contains seq/string/ref objects might not work. It might be not efficient if size of the object type is large and you frequently copy the function object.
Compile this with nim cpp -r --passC:"-std=gnu++17" testcpp.nim:
{.experimental: "callOperator".}
{.emit: """/*TYPESECTION*/
#include <functional>
template<typename T>
std::function<void()> bindProcAndObj(void (*functionPointer)(T&), const T& e)
{
return std::bind(functionPointer, e);
}
""".};
type
VoidFunction {.importcpp: "std::function<void()>", header: "<functional>".} = object
proc bindProcAndObj[T](p: proc (x: var T) {.nimcall.}, x: T): VoidFunction {.importcpp: "bindProcAndObj(@)".}
proc `()`(f: var VoidFunction) {.importcpp: "std::invoke(@)", header: "<functional>".}
# Testing
type
Foo = object
x: int
var y = bindProcAndObj((proc (x: var Foo) {.nimcall.} = inc x.x; echo x.x), Foo(x: 123))
y()
y()
Great thanks, working directly with nimcall and avoid splitting closures raw parts is way better !
This is what i'm looking for, looking good so far (but still a long way to go !!!):
import macros
{.emit: """/*TYPESECTION*/
#include <iostream>
#include <functional>
// This is part of a framework i can't modify
struct CppObjectBase
{
virtual ~CppObjectBase() = default;
virtual int overloadableFunction(float) = 0;
};
void callBase(CppObjectBase& obj)
{
std::cout << "C++ calling > " << obj.overloadableFunction(1.3f) << '\n';
}
// This is part of a nim binding i can tweak
template <class R, class... Args>
std::function<R(Args...)> bindClosure(R (*funcPtr)(Args...))
{
return [funcPtr](Args... args) -> R { return (*funcPtr)(args...); };
}
struct CppObject : CppObjectBase
{
std::function<int(float)> onOverloadableFunction;
int overloadableFunction(float a) override
{
if (onOverloadableFunction)
return onOverloadableFunction(a);
return {};
}
int y = 42;
};
""".};
macro exportCppFunction(body: untyped) =
if body.kind != nnkProcDef:
error "Invalid macro usage, needs to be attached to a proc", body
if len(body[3][1]) >= 9:
error "Too many arguments for proc", body
let functionIdent = body[0]
let makeFunctionIdent = newIdentNode("make" & $body[0])
var counter: int = 1
var templateResult = "\'*0"
var genericArgs = nnkIdentDefs.newTree(newIdentNode("R"))
var functionReturnType = nnkBracketExpr.newTree(functionIdent, newIdentNode("R"))
var formalParams = nnkFormalParams.newTree(newIdentNode("R"))
var templateArgs = ""
for i in 0 ..< len(body[3][1]) - 2:
genericArgs.add newIdentNode("T" & $counter)
functionReturnType.add newIdentNode("T" & $counter)
formalParams.add nnkIdentDefs.newTree(
newIdentNode("a" & $counter),
newIdentNode("T" & $counter),
newEmptyNode()
)
templateArgs &= "\'*" & $counter
if i < len(body[3][1]) - 3:
templateArgs &= ", "
inc(counter)
genericArgs.add newEmptyNode()
genericArgs.add newEmptyNode()
result = nnkStmtList.newTree(
nnkTypeSection.newTree(
nnkTypeDef.newTree(
nnkPragmaExpr.newTree(
functionIdent,
nnkPragma.newTree(
nnkExprColonExpr.newTree(
newIdentNode("importcpp"),
newLit("std::function<" & templateResult & "(" & templateArgs & ")>")
),
nnkExprColonExpr.newTree(
newIdentNode("header"),
newLit("<functional>")
)
)
),
nnkGenericParams.newTree(
genericArgs
),
nnkObjectTy.newTree(
newEmptyNode(),
newEmptyNode(),
newEmptyNode()
)
)
),
nnkProcDef.newTree(
makeFunctionIdent,
newEmptyNode(),
nnkGenericParams.newTree(
genericArgs
),
nnkFormalParams.newTree(
functionReturnType,
nnkIdentDefs.newTree(
newIdentNode("p"),
nnkProcTy.newTree(
formalParams,
nnkPragma.newTree(
newIdentNode("nimcall")
)
),
newEmptyNode()
)
),
nnkPragma.newTree(
nnkExprColonExpr.newTree(
newIdentNode("importcpp"),
newLit("bindClosure(@)")
)
),
newEmptyNode(),
newEmptyNode()
)
)
# Example usage
proc OverloadableFunction(float32): cint {.exportCppFunction.}
type
CppObjectBase {.importcpp: "CppObjectBase", inheritable.} = object
CppObject {.importcpp: "CppObject".} = object of CppObjectBase
onOverloadableFunction: OverloadableFunction[cint, float32]
y: cint
proc overloadableFunction(this: var CppObjectBase, arg: float32): cint {.importcpp: "#.overloadableFunction(@)"}
proc `callBase`(obj: var CppObjectBase) {.importcpp: "callBase(@)".}
var x = CppObject()
x.onOverloadableFunction = makeOverloadableFunction(proc(arg: float32): cint {.nimcall.} =
inc(x.y); echo "Nim calling > ", $arg; result = x.y)
echo x.overloadableFunction(4.2'f32)
callBase(x)
https://play.nim-lang.org/#ix=3WfB
If you try this example, it fails to compile, for some reason nim is not able to deduce the correct type of the passed procedure to bindNimClosure unless i fully specify the generics.
This doesn't work:
var myclos = bindNimClosure(my)
myclos(1)
But this works:
var myclos = bindNimClosure[void, int](my)
myclos(1)
Is there a way to help the compiler deduce the right type of the proc ?