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

C++ move semantics, copying, destructors, memory management #13

Open haxscramper opened 2 years ago

haxscramper commented 2 years ago
type
  NimObject {.exportc: "NimObject".} = object
    field: int

proc `=destroy`(obj: var NimObject) {.exportc: "destroy_NimObject".} =
  echo "Called nim destructor for ", obj.field

{.emit:"""/*TYPESECTION*/
template <typename T>
struct CxxContainer { T value; };

""".}

type
  CxxContainer[T] {.importcpp.} = object
    value: T

proc `=destroy`[T](obj: var CxxContainer[T]) =
  echo "Called destroy for cxx container"
  `=destroy`(obj.value)

# proc `=copy`(dest: var T; source: T)
proc `=copy`[T](dest: var CxxContainer[T], source: CxxContainer[T]) =
  echo "Copying cxx container"

# proc `=sink`(dest: var T; source: T)
proc `=sink`[T](dest: var CxxContainer[T], source: CxxContainer[T]) =
  echo "Moving cxx container"

proc main() =
  block:
    var c = CxxContainer[NimObject](value: NimObject(field: 2))
    var c1 = c

main()
#include <iostream>

struct NimObject {
    int field;
    ~NimObject() { std::cout << "Called destroy for " << field << "\n"; }
};

template <typename T>
struct CxxContainer { 
    T value; 
    CxxContainer(T&& _value) : value(_value) { }

    ~CxxContainer() { std::cout << "Called destroy for container\n"; }
    CxxContainer(CxxContainer&& other) = default;
    CxxContainer(const CxxContainer& other) { 
        value = other.value;
        std::cout << "Called copy on cxx container\n"; 
    }
};

int main() {
    CxxContainer<NimObject> c(NimObject{2});
    auto c1 = c;
}

In these two example nim and C++ code produce completely different results - nim calls destructor only once, does not invoke neither copy nor move.

    CxxContainer<NimObject> c(NimObject{2});
    auto c1 = c;
Called destroy for 2 // I'm not sure where this destroy call comes from
Called copy on cxx container
Called destroy for container
Called destroy for 2
Called destroy for container
Called destroy for 2
    var c = CxxContainer[NimObject](value: NimObject(field: 2))
    var c1 = c
Called destroy for cxx container
Called nim destructor for 2
haxscramper commented 2 years ago

Related

haxscramper commented 2 years ago

Main issue that arises from this difference - I need to figure out a way how to "mend together" nim and C++ object ownership/memory-management semantics. Nim (sadly) does not have precise tools for handling objects, like "function requires rvalue", "cannot copy", "cannot default-construct" (https://github.com/nim-lang/RFCs/issues/252) and the best nim counterpart for some of these cases would be just "API by convention", which would just lead users into codegen bugs.

haxscramper commented 2 years ago

Running with --expandArc:main shows that nim code infers c1 to be a cursor into c

block :tmp:
  var c
  c = CxxContainer[NimObject](value: NimObject(field: 2))
  var c1_cursor = c
  `=destroy`(c)
haxscramper commented 2 years ago

Partially related https://github.com/nim-lang/RFCs/issues/432 - if would be nice to have some form of "requires move" etc. annotations

How to prevent return value from been=copyed?

haxscramper commented 2 years ago

Two possible ways of implementing -

proc newImportAux*() {.importc: "//", header: "<new>".} =
  discard

proc newType(arg1, arg2...) ref Type =
  new(result)
  newImportAux()
  {.emit: "new ((void*)result) Type(`arg1`, `arg2`);.}

or

proc cxxPlaceNew*[T](x: ref T) {.
  header: "<new>", importcpp: "(new (#) '*0(@))", varargs.}

proc newType(arg1, arg2...) ref Type =
  new(result)
  cxxPlaceNew(arg1, arg2)

cxxPlaceNew should be a part of helper library, or automatically generated for the wrappers (#11 can setup everything, or code can be generated anew)

haxscramper commented 2 years ago