haxscramper / hcparse

High-level nim bindings for parsing C/C++ code
https://haxscramper.github.io/hcparse-doc/src/hcparse/libclang.html
Apache License 2.0
37 stars 2 forks source link

Overriding C++ clases on the nim side (current best implementation idea) #2

Open haxscramper opened 3 years ago

haxscramper commented 3 years ago

Base C++ class cppbase.cpp

#pragma once

#include <stdio.h>

struct CppBase {
  virtual void baseMethod(int arg) {
    printf("arg from nim - %d -\n", arg);
  }
};

Automatically derived generated file - generated for every class definition during wrapping process.

Generated derived class definition cppderived.hpp

#pragma once

#include <stdio.h>
#include "cppbase.hpp"

struct CppBaseDerived : public CppBase {
  // Callback for nim implementation.
  void (*baseMethodImpl)(void*, int);

  void baseMethodOverride(
    void* userData,  /// Custom user data
    int arg  /// Original argument to method
  );
};

Generated implementation for method implementations cppderived.cpp

#include "cppderived.hpp"

void CppBaseDerived::baseMethodOverride(void* userdata, int arg) {
    if (this->baseMethodImpl == 0) {
        puts("--- No override used, fallback to default implementation\n");
        CppBase::baseMethod(arg);

    } else {
        puts("--- Using nim implementation\n");
        this->baseMethodImpl(userdata, arg);
    }
}

Wrappers callbacks.nim

const derivedHeader* = "cppderived.hpp"

{.compile: "cppderived.cpp".}

type
  CppBaseDerivedRaw* {.
    importcpp: "CppBaseDerived",
    header: derivedHeader
  .} = object

    baseMethodImplProc* {.importcpp: "baseMethodImpl".}:
      proc(userData: pointer, arg: cint) {.cdecl.}

  CppBaseDerived*[T] = object
    ## Wrapper object might (in theory) also serve as a way to manage CPP
    ## objects using nim memory management. Destruction heap-allocated object
    ## will be performed on `destroy=` hook. Using composition instead of
    ## pointer to implementation is also possible.

    d*: ptr CppBaseDerivedRaw ## Pointer to raw object implementation

    userData*: T ## Custom user data

    # Callback closure implementation, separated into underlying parts.
    clos: tuple[
      # C function callback, with additional argument for closure environment
      impl: proc(this: var CppBaseDerived[T], arg: int, env: pointer) {.cdecl.},

      # Pointer to environment itself
      env: pointer
    ]

proc setBaseMethod*[T](
    self: var CppBaseDerived[T],
    cb: proc(this: var CppBaseDerived[T], arg: cint)
  ) =

  # `{.cdecl.}` implementation callback that will be passed back to
  # raw derived class
  let implCallback = proc(userData: pointer, arg: cint ): void {.cdecl.} =
    # Uncast pointer to derived class
    var derived = cast[ptr CppBaseDerived[T]](userData)

    # Call closure implementation, arguments and closure environment.
    derived.clos.impl(derived[], arg, derived.clos.env)

  self.d.baseMethodImplProc = implCallback
  self.clos.env = cb.rawEnv()
  self.clos.impl = cast[CppBaseDerived[T].clos.impl](cb.rawProc())

proc newCppBaseDerivedRaw(): ptr CppBaseDerivedRaw
  # Implementation for raw object
  {.
    importcpp: "new CppBaseDerived(@)",
    constructor,
    header: derivedHeader
  .}

proc newCppBaseDerived*[T](): CppBaseDerived[T] =
  ## Wrapper constructor. All implementation detauls for closure will be
  ## set using `setBaseMethod`, so we only initialize base object.
  CppBaseDerived[T](d: newCppBaseDerivedRaw())

proc baseMethod*[T](derived: var CppBaseDerived[T], arg: int): void =
  proc baseMethod(
    impl: ptr CppBaseDerivedRaw,
    userData: pointer,
    arg: int
  ): void {.importcpp: "#.baseMethodOverride(@)", header: derivedHeader.}

  baseMethod(derived.d, cast[pointer](addr derived), arg)

To override behavior of the class, you can set implementation callback to a new function:

main.nim

import callbacks

proc main() =
  var derived = newCppBaseDerived[int]()

  let capture = "hello"

  derived.setBaseMethod proc(this: var CppBaseDerived[int], arg: cint) =
      echo capture
      echo "Override callback with nim implementation", arg

  derived.baseMethod(12)

main()

But I still can provide override for the behavior of the object without actually overriding anything, which might be quite useful for various 'DelegatePainter' OOP patterns, where you actually only want to override implementation of a single method and nothing else. With support for passing user data, and setting closures as implementation (and not just {.cdecl.} callbacks) it won't be necessary to derive from C++ classes in most cases anyway.

-—

standalone classes

In rarer cases where you'd actually need to provide a full-fledged derived class, it is possible to implement some codegen facilities.

I couldn't find a way to generate standalone files that can be injected in nim object hierarchy (at least without some ugly hacks). In order to derive from C++ class, I would generate actual C++ class via nim macros, similarly to nim by example macros.

cxxClass NewCxx of CppDerived:
  field: int
  proc newMethod(): NI

Will generate following C++ code:

class NewCxx : public CppDerived {
  NI field;
  NI newMethod(){
    return newMethod_nimImpl(); // Actual implementation of nim method is
                                // declared in nim code.
  }
}

Actually generating C++ code also helps with Qt - I no longer need to reimplement MOC, and instead can just use it as-is.

haxscramper commented 3 years ago

After some testing with codegen I think it is the best solution overall, but there are some issues, like making nim-declared types available in generated C++ code. If NewCxx uses non-trivial nim type - how to make it available in generated header?

Also, 'derived' classes still won't behave as proper OOP on the nim side - e.g. I'd need to provide additional overloads for all procs. -- this seems to be partially covered by #1, but needs more feature-testing.