Search code examples
androidc++djinni

Alternative to interface inheritance with Djinni


I am using Djinni to share some big c++ codebase among android and ios. One of the various components (let's call it Foo!!!) has a different implementation on android and ios. Foo is an interface, which the c++ code uses without the need of knowing anything about its internals.

The android implementation though (FooAndroid), has some additional methods that the android client can use to modify the behaviour of the component in ways only meaningful to the android platform.

This complicates things with djinni, because of the lack of interface inheritance. I came up with a solution of sorts, that relies on the fact that FooAndroid can subclass two different djinni interfaces that have most of their methods with identical signature, but the result is not pretty.

This is the djinni interface description:

foo = interface +c {
    static create() : foo;
    doSomething();
}

foo_android = interface +c {
    static create() : foo_android;
    doSomething();
    doAnotherThing();
    asFoo(): foo;
}

bar = interface +c {
    static create() : bar;
    useFoo(f: foo);
}

You'll notice Bar a component that just needs to access a Foo instance.

The resulting interfaces are then implemented in this way:

// Foo android implementation:

class FooAndroidImpl : public Foo, public FooAndroid {
public:
    void doSomething() override { LOGI("do something"); }
    void doAnotherThing() override { LOGI(" do something"); }

    std::weak_ptr<Api::Foo> fooSelfReference;

    std::shared_ptr<Api::Foo> asFoo() override { return fooSelfRef.lock(); }

};

// instance creation methods:

std::shared_ptr<FooAndroidImpl> createImpl() {
    auto p = std::make_shared<FooAndroidImpl>();
    p->fooSelfRef = p;
    return p;
}

std::shared_ptr<FooAndroid> FooAndroid::create()
{
    return createImpl();
}

std::shared_ptr<Foo> Foo::create()
{
    return createImpl();
}

// Bar implementation:

class BarImpl : public Bar {
public:
    void useFoo(const std::shared_ptr<Foo> & f) override { f->doSomething(); }
};

std::shared_ptr<Bar> Bar::create() 
{ 
    return std::make_shared<BarImpl>();
}

I am then able to use FooAndroid and Bar in java in this way:

FooAndroid fooAndroid = FooAndroid.create();
fooAndroid.doAnotherThing();
Bar bar = Bar.create();
bar.useFoo(fooAndroid.asFoo());

This works, but it's ugly, I have to define two almost identical djinni interfaces by hand, and I am not still sure of all the implications of storing that weak_ptr on the lifecycle of the object.

I feel I am abusing Djinni's purpose and model, so maybe there's a better way to achieve what I am trying to do?


Solution

  • Djinni doesn't support interface inheritance, so there's not a great answer here. At Dropbox this doesn't come up often, and we usually address it by just adding Android/iOS specific methods to a single interface, and implementing them with stubs which throw an exception on the other platform. We've usually run into the reverse case, though, where the implementation was in Java/ObjC.

    If you don't like that approach, and don't like having the method duplication and multiple inheritance I think the other approach I'd recommend would be to use related subobjects. E.g. have a Foo interface with a getAndroidStuff() method, which returns a FooAndroid interface containing only the Android-specific methods. The FooAndroid object can internally hold a reference back to the FooImpl object which has all the functionality, and forward methods to it.

    It seems like the broader question here is what's really platform-specific about these methods? If the implementation is cross-platform, but only needed on one platform, then it doesn't hurt to just let a single C++ class stand in, and the unused methods are non-dangerous. If you've got platform-specific code in C++ then things get a bit awkward since that wasn't the design goal of Djinni. I think this specific case wants a solution different from the general case of interface inheritance, though.