How to deal with covariant return types with SWIG for C#?

Viewed 70

I am currently using SWIG to generate C# bindings for a third party C++ codebase. I've mostly gotten things working, but there is one thing I'm struggling with. In the codebase, there is a "clone()" method in many of their classes which returns a copy of the calling object. The method uses covariant return types in the C++ code to return an object of the given class's type, but, due to C# not supporting covariant return types, the return types of the clone methods in the generated C# code are all that of the base class.

Is there a method within SWIG to change the return type of all "clone()" methods to match that of the class in which they reside?

I tried using typemap with

%typemap(cstype) *::clone "$csclassname"

and similar variations, but received errors when trying to generate the bindings.

The only "working" method I've found is to use

%ignore *::clone;

then use typemap(cscode) to redefine the clone method with the correct type for all classes that use it, but that is extremely tedious.

I think I just don't completely understand how SWIG typemaps work/if they can work with specific methods like this. If anyone has suggestions for a better way to do this and/or an explanation that could help clarify things for me, it would be greatly appreciated. Thank you.

Edit: Adjusted description to identify the problem as C# not supporting covariant return types.

Edit 2: To clarify, C# 9.0 supports covariant return types, but earlier versions do not. I am using an earlier version for compatibility with a Unity project. I may be able to upgrade the project and my C# version for covariant return type compatibility, though I'm not sure if SWIG's C# tool is compatible with C# 9.0. I wanted to check if there is a workable SWIG solution for my current C# and Unity versions before pursuing that potential option.

Sample Code to Reproduce Issue:

Parent.h

#ifndef PARENT_H
#define PARENT_H
class Parent
{
    public:
        int val1;
        Parent::Parent() = default;
        Parent::Parent(const Parent &copiedObject);
        virtual Parent* clone() const;
};
#endif

Parent.cpp

#include "Parent.h"
Parent::Parent(const Parent &copiedObject)
{
    val1 = copiedObject.val1;
}
Parent* Parent::clone() const
{
    Parent* clone = new Parent(*this);
    return clone;
}

Child.h

#ifndef CHILD_H
#define CHILD_H
#include "Parent.h"
class Child : public Parent
{
    public:
        int val2;
        Child::Child() = default;
        Child::Child(const Child &copiedObject);
        Child* clone() const override;
};
#endif

Child.cpp

#include "Child.h"
Child::Child(const Child &copiedObject) : Parent(copiedObject)
{
    val1 = copiedObject.val1;
    val2 = copiedObject.val2;
}
Child* Child::clone() const
{
    Child* clone = new Child(*this);
    return clone;
}

CSharpModule.i

%module CSharpModule
%{
#include "Parent.h"
#include "Child.h"
%}

%newobject *::clone;
%include "Parent.h"
%include "Child.h"

clone() method in generated Child.cs

public override Parent clone() {
    global::System.IntPtr cPtr = CSharpModulePINVOKE.Child_clone(swigCPtr);
    Child ret = (cPtr == global::System.IntPtr.Zero) ? null : new Child(cPtr, true);
    return ret;
}

Due to lack of support for covariant return types in C#, the clone() method would need to change to "new" instead of "override (SWIG's %csmethodmodifiers directive) in the derived class:

Desired clone() method

public new Child clone() {
    global::System.IntPtr cPtr = CSharpModulePINVOKE.Child_clone(swigCPtr);
    Child ret = (cPtr == global::System.IntPtr.Zero) ? null : new Child(cPtr, true);
    return ret;
}
1 Answers

I don't know where you get your information, but your entire premise is wrong. C# definitely supports covariant return types:

    abstract class B 
    {
        public abstract B Build();
    }
    
    class D : B
    {
        public override D Build() => new();
    }

That said, this feature in either language adds essentially nothing, since any "correct" OOP program will use the base class' (or interface's) methods. If you find yourself constantly down-casting to get the more specific version of the function, you should probably rethink your design.

After all, the code generated is identical with or without this feature. This is a compile-time only feature.

Related