Help Wrapping simple Class
Closed this issue · 11 comments
Hello - thank you for providing this awesome solution for adding capabilities to micropython. I have cloned the repo and used "make teststaticlib" successfully. I wanted to move on to an uncomplicated class to use as practice before implementing a more complicated class. However, I'm having trouble implementing this on even the uncomplicated class. I've tried to correct my code based on the compiler errors displayed. But, I'm still missing something.
The uncomplicated class I would like to practice wrapping is from https://github.com/sdumetz/coordinates/tree/master/src
The header file is:
#ifndef _Coordinates_h
#define Coordinates_h
#include "math.h"
#ifndef PI
#define PI 3.1415926535897932384626433832795
#endif
class Coordinates {
public:
// start value, start speed, update confidence, evolution confidence
Coordinates(float x = 0, float y = 0);
void fromCartesian(float x, float y);
void fromPolar(float r, float phi);
float getR() {
return r;
};
float getAngle() {
return phi;
};
float getX() {
return x;
};
float getY() {
return y;
};
private:
float r;
float phi;
float x;
float y;
};
#endif
Could you help point me in the right direction on how I can wrap this class? Maybe I can write up a tutorial based on the help that you give me. Once I get the right idea, I'm hoping to create a few more tutorials to help those interested.
Thank You
Can you explain what exactly the problem is? There's nothing different for this class than for the Simple
class in this repository so it just works (you'd just have to mke getAngle
etc const
if you want to use ClassWrapper::Getter
.
Sidenote: your class design is a bit strange: if you just instantiate a Coordinate directly it's cartesian but to get a polar one you first have to make a cartesian coordinate and then discard it's value and call fromPolar. Might be worth looking into a more standard approach i.e. separate CartesianCoordinate and PolarCoordinate classes which can be converted to one another. Oh and pi
is 4 * std::atan(1.0)
:)
Thanks for the reply. I forgot to include the error messages in the previous post. I will keep trying to get it to work. It's probably something dumb that I'm doing. I'll let you know in a few days if I'm successful. If not, I'll post the error messages I get from make.
Thanks for the tip about pi.
Apparently I was having some kind of synchronization issue with the wsl terminal and what I was seeing in Windows explorer. After I closed everything and restarted I was able to get the class wrapped per the instructions provided. In case anyone else is interested, I ended up with the following module.cpp:
#include "../classwrapper.h"
#include "class.h"
using namespace upywrap;
struct F
{
func_name_def( fromCartesian )
func_name_def( fromPolar )
func_name_def( getX )
func_name_def( getY )
func_name_def( getR )
func_name_def( getAngle )
};
extern "C"
{
void doinit_upywraptest( mp_obj_module_t* mod )
{
upywrap::InitializePyObjectStore( *mod );
upywrap::ClassWrapper< Coordinates > wrap1( "Coords", mod );
wrap1.DefInit<float, float>();
wrap1.Def< F::fromCartesian >( &Coordinates::fromCartesian );
wrap1.Def< F::fromPolar >( &Coordinates::fromPolar );
wrap1.Def< F::getR >( &Coordinates::getR );
wrap1.Def< F::getAngle >( &Coordinates::getAngle );
wrap1.Def< F::getX >( &Coordinates::getX );
wrap1.Def< F::getY >( &Coordinates::getY );
}
mp_obj_module_t* init_upywraptest()
{
auto mod = upywrap::CreateModule( "arduino", false );
doinit_upywraptest( mod );
return mod;
}
}
I also wrote a new class.py test as follows:
import arduino
geo = arduino.Coords(12,34)
print(geo.getX())
print(geo.getY())
geo.fromCartesian(43,21)
print(geo.getR())
print(geo.getAngle())
geo.fromPolar(14.14213562011719, 0.7853981852531433)
print(geo.getX())
print(geo.getY())
With class.py.exp as follows:
12.0
34.0
47.85394287109376
0.4543020725250244
10.0
10.0
Now when I run "make teststaticlib", micropython compiles and the test passes.
I now feel confident enough to try wrapping a more complicated class.
One more question: When I run "make staticlib" I get module_static.o and libupywraptest.a in the tests folder. How do I go about adding those files to the micropython repo and then using the standard make file for the unix port? The comments in the Makefile state "patch micropython to initialize and register the upywraptest module from the static lib in main". But I'm not sure how to actually do that.
My goal is to wrap a bunch of arduino libraries into static ones that I can then add to the unix port and compile using the Makefile in the micropython/ports/unix folder. I would also like to try adding the libraries to other ports if I'm successful with the unix port.
Thank you, for your help.
patch micropython
Sorry that's an oversight, forgot to update the README after commit 26fcd5a where I removed said patching from the Makefile. As you can see in the diff it's just a matter of adding a line with mp_module_register(...
into unix/main.c. And since MicroPython recently got support for building C++ directly it should be possible to pass the appropriate flags and files to compile on the commandline so you don't need to build the .lib separately.
compile using the Makefile in the micropython/ports/unix folder.
I've recently added support for this by using the 'User C Module' (https://github.com/micropython/micropython/blob/master/docs/develop/cmodules.rst) feature from MicroPython so you don't have to modify any file in MicroPython but only pass some flags to make
to point it to your module files. This isn't in master yet because no-one else tested it yet and because module registration is a bit finicky. However it does work, I tested it on your code: https://gist.github.com/stinos/c9a3dbb8438788799225649aa90441d8. Would be great if you can test this. You will need the directory structure and files from the gist. And the esp-32-C++ branch from this repository (don't min the esp32, works for unix as well)
Unfortunately it didn't work.
This is the branch I used:
tron@SkyNet:~/projects/micropython-wrap$ git branch -a
* esp32-c++
remotes/origin/esp32-c++
The following is how I set up the file structure:
tron@SkyNet:~/projects$ tree
.
└── projects
├── micropython
├── micropython-wrap
└── upymodules
tron@SkyNet:~/projects/upymodules$ tree
.
└── arduino
├── Coordinates.cpp
├── Coordinates.h
├── cmodule.c
├── micropython.mk
└── module.cpp
This is the error I'm getting:
tron@SkyNet:~/projects/micropython/ports/unix$ make USER_C_MODULES=../../../upymodules CFLAGS_EXTRA="-DMODULE_ARDUINO_ENABLED=1 -DMICROPY_MODULE_BUILTIN_INIT=1" -j4
Use make V=1 or set BUILD_VERBOSE in your environment to increase build verbosity.
Including User C Module from ../../../upymodules/arduino
make: *** No rule to make target '/home/tron/projects/micropython/ports/unix/cmodule.c', needed by 'build-standard/genhdr/qstr.i.last'. Stop.
I also get the following if I just run make:
sofoli@SkyNet:~/projects/micropython/ports/unix$ make
Use make V=1 or set BUILD_VERBOSE in your environment to increase build verbosity.
make: *** No rule to make target 'lib/axtls/ssl/asn1.c', needed by 'build-standard/genhdr/qstr.i.last'. Stop.
However, it does compile if I run "make usermodule" from the micropython-wrap directory:
tron@SkyNet:~/projects/micropython-wrap$ make usercmodule
make -C ../micropython/ports/unix -j4 V=0 MICROPY_PY_BTREE=0 MICROPY_PY_FFI=0 MICROPY_PY_USSL=0 MICROPY_PY_AXTLS=0 MICROPY_FATFS=0 MICROPY_PY_THREAD=0 USER_C_MODULES=/home/tron/projects/micropython-wrap CFLAGS_EXTRA="-DMODULE_UPYWRAPTEST_ENABLED=1 -DMICROPY_MODULE_BUILTIN_INIT=1" BUILD=build-usercmod UPYWRAP_BUILD_CPPMODULE=1 UPYWRAP_PORT_DIR=../micropython/ports/unix all
make[1]: Entering directory '/home/tron/projects/micropython/ports/unix'
Use make V=1 or set BUILD_VERBOSE in your environment to increase build verbosity.
Including User C Module from /home/tron/projects/micropython-wrap/tests
mkdir -p build-usercmod/genhdr
mkdir -p build-usercmod/
mkdir -p build-usercmod/build-usercmod/
mkdir -p build-usercmod/extmod/
mkdir -p build-usercmod/lib/embed/
mkdir -p build-usercmod/lib/mp-readline/
mkdir -p build-usercmod/lib/timeutils/
mkdir -p build-usercmod/lib/utils/
mkdir -p build-usercmod/py/
mkdir -p build-usercmod/tests/
...
...
...
CC modusocket.c
CXX /home/tron/projects/micropython-wrap/tests/module.cpp
CC ../../lib/mp-readline/readline.c
CC ../../lib/timeutils/timeutils.c
CC ../../lib/utils/gchelper_generic.c
CC build-usercmod/frozen_content.c
LINK micropython
text data bss dec hex filename
509194 56384 4656 570234 8b37a micropython
make[1]: Leaving directory '/home/tron/projects/micropython/ports/unix'
But obviously, that doesn't make what we were working towards.
What do you think I did wrong while following your instructions?
I don't think you're doing anything wrong, I just missed an instruction :) see below. But apart from that, using the exact make command as you show works fine for me. The only think I can think of now is you're using a different MicroPython version (I'm using the last master), or there's some strange bug in it.
No rule to make target '/home/tron/projects/micropython/ports/unix/cmodule.c'
This is especially strange since it means the $(MOD_DIR)/cmodule.c
in micropython.mk doesn't get expanded as it should. You don't have any other modules in the upymodules directory, right?
make: *** No rule to make target 'lib/axtls/ssl/asn1.c'
This is because the axtls submodule isn't initialized. the problem doesn't occur in make usercmodule
because it passes MICROPY_PY_AXTLS=0
to the unix build. See e.g. micropython/micropython#5207, you should run git submodule update --init
to get all submodules.
I double checked the micropython.mk file and found a goof. I fixed the makefile and got all the submodules with the command you provided. I also had to build mpy-cross before building micropython again since I started with a new clone of the repo.
tron@SkyNet:~/projects/micropython/ports/unix$ make USER_C_MODULES=../../../upymodules CFLAGS_EXTRA="-DMODULE_ARDUINO_ENABLED=1 -DMICROPY_MODULE_BUILTIN_INIT=1" -j4
Use make V=1 or set BUILD_VERBOSE in your environment to increase build verbosity.
Including User C Module from ../../../upymodules/arduino
mkdir -p build-standard/genhdr
mkdir -p build-standard/
mkdir -p build-standard/arduino/
mkdir -p build-standard/build-standard/
...
...
...
CC modffi.c
CXX ../../../upymodules/arduino/module.cpp
CXX ../../../upymodules/arduino/Coordinates.cpp
CC ../../lib/mp-readline/readline.c
CC ../../lib/timeutils/timeutils.c
CC ../../lib/utils/gchelper_generic.c
CC build-standard/frozen_content.c
LINK micropython
text data bss dec hex filename
462839 56592 2464 521895 7f6a7 micropython
It worked! And to confirm:
tron@SkyNet:~/projects/micropython/ports/unix$ ./micropython
MicroPython v1.13-243-g032e09562 on 2020-12-09; linux version
Use Ctrl-D to exit, Ctrl-E for paste mode
>>> import arduino
>>> dir(arduino)
['__class__', '__name__', 'Coords', 'Coords_locals']
>>> geo = arduino.Coords(12, 34)
>>> print(geo.getR())
36.05551147460938
>>> print(geo.getAngle())
1.23150372505188
That's awesome! I am curious as to how the micropython-wrap repo is used in the process. We put all the files in the ~/projects/upymodules/arduino/ directory and ran the make from the ~/projects/micropython/ports/unix directory.
I assume that make reaches into micropython-wrap at some point to get the classwrapper (and other supporting) header files. Could you explain it a bit so its not so fuzzy to me?
Also, please let me know if you need some more testing done.
Could you explain it a bit
micropython-wrap is only header files, so the only thing which the compiler needs for module.cpp to compile is being able to find the <micropython-wrap/classwrapper.h> include file. This is done in micropython.mk by adding -I../../../
which happens to work because it will resolve to ~/projects since make
was called in the unix port directory, which is 3 levels deeper. Could also just use -I$HOME/projects
but that's less generic. Or -I$(TOP)/..
because the MicroPython Makefile defines TOP as it's root directory, etc..
Apart from that, cmodule.c has the mechanism to translate import arduino
into getting doinit_module
called to populate the module. It's a bit unfortunate that file is needed, and that each separate module needs its own copy, but I currently don't see another way (at least not without altering MicroPython).
please let me know if you need some more testing done.
Not really, I'm glad it works not only for me so thanks for testing so far :)
I was able to get multiple modules to work by making another directory in the upymodules directory. I did have to make sure the module name and functions to initialize it had unique names. It worked great.
When do you think you will put this capability into the master repo?
Just merged it, don't think there's a reason to hold it back.
I did have to make sure the module name and functions to initialize it had unique names.
Yes indeed, no way around that since all modules get built into the same executable.
Awesome - Thank you for all your help.
I'm closing the issue as you've already merged.