wlav / cppyy

Other
391 stars 40 forks source link

How to use and deal with const char *? #121

Open jak6jak opened 1 year ago

jak6jak commented 1 year ago

I am using cppyy to create bindings for flecs: https://github.com/SanderMertens/flecs which is a C library with a C++ interface option. Unfortunately, this library is an avid user of const char * strings. Sometimes I am able to use string_view other times that does not work. Here is some simple code I am trying to get to work:

def hello_world():
    print(dir(flecs.world))

    cppyy.ll.set_signals_as_exception(True)
    with cppyy.ll.signals_as_exception():
        world = flecs.world(cppyy.gbl.ecs_init())
        e = world.entity(flecs.string_view("Bob")) #works
        # e = world.entity(str("Bob")) #fails Template method resolution Failed to instantiate "entity(std::string)"
        print(e.is_alive())
        print(e.name())

        # g = world.lookup(flecs.string_view("Bob")) #TypeError: could not convert argument 1 (bad argument type for built-in operation)
        g= world.lookup(str("Bob")) #works
        print(g.name())

        #cppyy.gbl.ecs_fini(world)
        pass

I am confused on how I should be passing and using strings for methods and classes that require const char *. right now it seems pretty inconsistent on what I should be using.

wlav commented 1 year ago

Using const char* is difficult b/c ownership isn't clear from reflection only and b/c Python's strings are unicode, not single byte chars. Thus, for example, using std::string_view like above has no zero-copy benefits: the Python unicode needs to be converted anyway (assumes UTF-8 by default) and some buffer needs to be managed (cppyy actually does this for std::string_view). Also str("Bob") gives the same type as "Bob" but at the cost of another superfluous copy.

The only way to get the same level of control over const char* buffers as in C, however, is to use Python's ctypes, which cppyy accepts for function arguments, variables, etc., and to pick your encoding or use Python's bytes type.

As for the templates, make sure that the template arguments can actually be deduced, or specify them explicitly using []. I'd be surprised if the template argument types are const char*, though. But if they are, yes, cppyy does not try that possibility automatically: it will default to std::string.

I'm not otherwise familiar with flecs.

jak6jak commented 1 year ago

okay I think I understand what you are saying. entity is defined as thus:

/** Create an entity.
 * 
 * \memberof flecs::world
 * \ingroup cpp_entities
 */
template <typename... Args>
flecs::entity entity(Args &&... args) const;

I have modified my code to be this:

def hello_world():
    #print(dir(flecs.world))

    cppyy.ll.set_signals_as_exception(True)
    with cppyy.ll.signals_as_exception():
        world = flecs.world(cppyy.gbl.ecs_init())
        e = world.entity["const char *"](cppyy.ctypes.c_char_p(bytes("Bob","ascii")))
        print(e.is_alive())
        print(e.name())

        g = world.lookup("Bob")
        print(g.name(), "Look up")

it seems a bit verbose but it works as far as I can tell. Am I doing anything redundant or misunderstanding anything?

wlav commented 1 year ago

Well, I'm not exactly following. If the declaration of entity is simply as above, than it should "just work". Example:

import cppyy

cppyy.cppdef(r"""\
class World {
public:
    template <typename... Args>
    std::string entity(Args&&... args) const {
        std::ostringstream o;
        ((o << args << ' '),...);
        return o.str();
    }
};""")

world = cppyy.gbl.World()
print(world.entity("Bob"))
print(world.entity("Bob", "Alice"))

But do note that the above does not require any const char*. If you print the doc after the calls:

print(world.entity.__doc__)

you'll see that the template is instantiated with std::string's, not const char*'s.

So, my best guess is that there is something in the implementation body of entity that absolutely requires these to be C strings (or otherwise runs in to SFINAE and a silent error). That might also explain why you see different behaviors for different types across different functions, depending on what happens to "fit."

And yes, this:

import ctypes
print(world.entity["const char*"](ctypes.c_char_p(bytes("Bob","ascii"))))

also works with the example above, but it's not necessary.

That said, supplying either ctypes.c_char_p(bytes("Bob","ascii") or ["const char*"] alone doesn't seem to work (for different reasons each), which is something that I can fix. In particular, supporting this world.entity["const char*"]("Bob") is easily done (just under the assumptions that a) UTF-8 is fine and b) no pointer to the buffered C string is stored).

wlav commented 1 year ago

I pushed some fixes to the repo to make both usage of world.entity["const char*"]("Bob") and world.entity(ctypes.c_char_p(bytes("Bob","ascii"))) behave as expected.

wlav commented 1 year ago

The fixes mentioned above a released with cppyy 3.0.0 and its dependencies.