Using CPP library in PlatformIO

Hi, I’m trying to use a Cpp library in PlatformIO (webern/wx). I’ve modified my platformio.ini to support C++ 17:

[env:esp32doit-devkit-v1]
platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2idf/platform-espressif32-2.0.2.zip
board = esp32doit-devkit-v1
framework = arduino
build_unflags = -std=gnu++11
build_flags = -std=gnu++17

monitor_speed = 115200

extra_scripts =  ...
lib_deps =  ...

I’ve created a script that copies all the files from a git submodule at my project’s root /dependencies/mx/Sourcecode/include/mx/api into include/mx. And another that replaces all the paths in the header files from mx/api/ScoreData.h, for example, into ScoreData.h.

The issue comes, of course, that I’m not copying the source code files, just the headers. How am I supposed to do this? Simply copy all the cpp files from mx/Sourcecode/private to the include directory? And updating my patching scripts to break all the structure of the folders where the cpp files are located at. I feel that this is quite incorrect, and there should be another way of using this library.

Hope I’m explaining well, and thanks in advance for the answer.

Well, done that, don’t working. I believe it’s because of the cpp files somehow not being imported correctly when including header files.

I’ve created an script that copies all the source files. It’s placed in dependencies and called buildmx.sh:

#!/usr/bin/env bash

rm -rf ../../include/mx/
mkdir ../../include/mx/

echo "🚚 Copying header files..."
cp -R ../mx/Sourcecode/include/mx/api/* ../../include/mx/

echo "🚚 Copying mx/api files..."
cp -R ../mx/Sourcecode/private/mx/api/* ../../include/mx/

echo "🚚 Copying mx/ezxml files..."
cp -R ../mx/Sourcecode/private/mx/ezxml/src/include/ezxml/* ../../include/mx/
cp -R ../mx/Sourcecode/private/mx/ezxml/src/private/private/* ../../include/mx/

echo "🚚 Copying mx/core files..."
cp -R ../mx/Sourcecode/private/mx/core/elements/* ../../include/mx/
cp -R ../mx/Sourcecode/private/mx/core/* ../../include/mx/
rm -rf ../../include/mx/elements

echo "🚚 Copying mx/impl files..."
cp -R ../mx/Sourcecode/private/mx/impl/* ../../include/mx/

echo "🚚 Copying mx/utility files..."
cp -R ../mx/Sourcecode/private/mx/utility/* ../../include/mx/

Then I have check-dependencies.py on the root of the project:

import os
import subprocess
import sys

print("Checking if MX is available...")

mx_dir = "./include/mx"
buildmx_dir = './dependencies/buildmx.sh'

if not os.path.exists(buildmx_dir):
    print("⚠️ buildmx dir doesn't exist.")
    sys.exit()

if not os.path.exists(mx_dir):
    print("❌ MX not available. Building...")
    subprocess.call(['sh', buildmx_dir])
    exec(open("./fix-mx-paths.py").read())
else:
    print("✅ MX is available")

And fix-mx-paths.py together with the previous file:

import os

mx_path = './include/mx'

files_list = os.listdir(mx_path)

print("🔃 Fixing compatibility for header files...")

for file in files_list:
    print(f"  📝 Patching {file}...", end = '')

    # Read the file data
    with open(f"{mx_path}/{file}", 'r') as rstream:
        filedata = rstream.read()

    # Replace all the desired strings
    filedata = filedata.replace('mx/api/', '')
    filedata = filedata.replace('mx/ezxml/', '')
    filedata = filedata.replace('mx/core/elements/', '')
    filedata = filedata.replace('mx/core/', '')
    filedata = filedata.replace('mx/impl/', '')
    filedata = filedata.replace('mx/utility/', '')
    filedata = filedata.replace('ezxml/', '')

    # Write the changes
    with open(f"{mx_path}/{file}", 'w') as wstream:
        wstream.write(filedata)

    print("ok")

My complete platformio.ini file ends up being:

[env:esp32doit-devkit-v1]
platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2idf/platform-espressif32-2.0.2.zip
board = esp32doit-devkit-v1
framework = arduino
build_unflags =
	-std=gnu++11
	-fno-exceptions
build_flags =
	-std=gnu++17
	-fexceptions

monitor_speed = 115200

extra_scripts = 
	pre:./install-dependencies.py
	pre:./load_pages.py
	pre:./check-dependencies.py

lib_deps = 
	esphome/AsyncTCP-esphome@^1.2.2
	ottowinter/ESPAsyncWebServer-esphome@^2.1.0
	ayushsharma82/AsyncElegantOTA@^2.2.6

install-dependencies.py and load_pages.py are some scripts for compiling web pages for using in web server.

I’m including the header files and using the example provided by mx in the following file (musicxml.h):

#include <fstream>
#include "mx/DocumentManager.h"
#include "mx/ScoreData.h"

...

int loadMusic(String path)
{
    ...

    // Once the XML is read, parse it
    using namespace mx::api;

    // Create a reference to the singleton which holds documents in memory for us
    auto &mgr = DocumentManager::getInstance();
    std::ifstream istr(path.c_str());

    // Ask the document manager to parse the xml into memory for us, returns a document ID.
    const auto documentId = mgr.createFromStream(istr);

    // Get the structural representation of the score from the document manager
    const auto score = mgr.getData(documentId);

    // We need to explicitly destroy the document from memory
    mgr.destroyDocument(documentId);

    if (score.parts.size() != 1)
        return LOAD_MUSIC_RESULT_FAIL;

    // drill down into the data structure to retrieve the note
    const auto &part = score.parts.at(0);
    const auto &measure = part.measures.at(0);
    const auto &staff = measure.staves.at(0);
    const auto &voice = staff.voices.at(0);
    const auto &note = voice.notes.at(0);

    if (note.durationData.durationName != DurationName::whole ||
        note.pitchData.step != Step::c)
        return LOAD_MUSIC_RESULT_FAIL;

    return LOAD_MUSIC_RESULT_OK;
}

However, I get the following error:

~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o:(.literal._ZN2mx3api15DocumentManager16createFromStreamERSi+0x28): undefined reference to `ezxml::XFactory::makeXDoc()'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o:(.literal._ZN2mx3api15DocumentManager16createFromStreamERSi+0x2c): undefined reference to `mx::core::makeDocument()'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o:(.literal._ZN2mx3api15DocumentManager16createFromStreamERSi+0x34): undefined reference to `mx::core::Document::fromXDoc(std::basic_ostream<char, std::char_traits<char> >&, ezxml::XDoc const&)'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o:(.literal._ZNK2mx3api15DocumentManager7getDataEi+0x4): undefined reference to `mx::core::Document::getChoice() const'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o:(.literal._ZNK2mx3api15DocumentManager7getDataEi+0x8): undefined reference to `mx::core::Document::convertContents()'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o:(.literal._ZNK2mx3api15DocumentManager7getDataEi+0xc): undefined reference to `mx::core::Document::getScorePartwise() const'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o:(.literal._ZNK2mx3api15DocumentManager7getDataEi+0x10): undefined reference to `mx::impl::ScoreReader::ScoreReader(mx::core::ScorePartwise const&)'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o:(.literal._ZNK2mx3api15DocumentManager7getDataEi+0x14): undefined reference to `mx::impl::ScoreReader::getScoreData() const'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o: in function `mx::api::DocumentManager::createFromStream(std::basic_istream<char, std::char_traits<char> >&)':
.../include/mx/DocumentManager.cpp:110: undefined reference to `ezxml::XFactory::makeXDoc()'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .../include/mx/DocumentManager.cpp:112: undefined reference to `mx::core::makeDocument()'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .../include/mx/DocumentManager.cpp:115: undefined reference to `mx::core::Document::fromXDoc(std::basic_ostream<char, std::char_traits<char> >&, ezxml::XDoc const&)'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .pio/build/esp32doit-devkit-v1/src/main.cpp.o: in function `mx::api::DocumentManager::getData(int) const':
.../include/mx/DocumentManager.cpp:194: undefined reference to `mx::core::Document::getChoice() const'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .../include/mx/DocumentManager.cpp:197: undefined reference to `mx::core::Document::convertContents()'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .../include/mx/DocumentManager.cpp:200: undefined reference to `mx::core::Document::getScorePartwise() const'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .../include/mx/DocumentManager.cpp:200: undefined reference to `mx::impl::ScoreReader::ScoreReader(mx::core::ScorePartwise const&)'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .../include/mx/DocumentManager.cpp:201: undefined reference to `mx::impl::ScoreReader::getScoreData() const'
~/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld: .../include/mx/DocumentManager.cpp:206: undefined reference to `mx::core::Document::convertContents()'

Hope this helps clearing up what am I doing.

Thanks for the help :face_with_thermometer:

If it finds the headers but not the implementation, it does not build the actual source files. I’d recommend adding the library the normal way by placing it in the lib/ folder of the project. Otherwise, use env.BuildSources() as documented, or env.BuildLibrary() if you want to pre-link it to a .a file.

Yeah, I understand. The issue is that this library “is not meant” to be used with PlatformIO, it’s a generic C++ library. Therefore, the folder structure is not “correct”.

As far as I’m concerned PIO libraries should have the structure:

|--lib
|  |
|  |--Bar
|  |  |--docs
|  |  |--examples
|  |  |--src
|  |     |- Bar.c
|  |     |- Bar.h
|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|  |
|  |--Foo
|  |  |- Foo.c
|  |  |- Foo.h
|  |
|  |- README --> THIS FILE
|

My library has the following structure:

mx
├── CodeGen
│   ├── src
│   │   ├── bin
│   │   ├── generate
│   │   │   ├── cpp
│   │   │   └── data
│   │   ├── model
│   │   │   └── default_create
│   │   ├── utils
│   │   └── xsd
│   └── tests
├── DevScripts
├── Documents
│   ├── DevNotes
│   ├── Licenses
│   └── OldCode
├── Resources
│   ├── custom
│   ├── expected
│   ├── foundsuite
│   ├── generalxml
│   ├── ksuite
│   ├── logicpro
│   ├── lysuite
│   ├── mjbsuite
│   ├── musuite
│   ├── mxl
│   ├── recsuite
│   ├── smufl
│   └── testOutput
├── Sourcecode
│   ├── include
│   │   └── mx
│   │       └── api
│   └── private
│       ├── cpul
│       ├── mx
│       │   ├── api
│       │   ├── core
│       │   │   └── elements
│       │   ├── examples
│       │   ├── ezxml
│       │   │   ├── other
│       │   │   └── src
│       │   │       ├── include
│       │   │       │   └── ezxml
│       │   │       └── private
│       │   │           └── private
│       │   ├── impl
│       │   └── utility
│       └── mxtest
│           ├── api
│           ├── control
│           ├── core
│           ├── file
│           ├── impl
│           └── import
└── Xcode
    ├── Mx.xcodeproj
    │   ├── project.xcworkspace
    │   └── xcshareddata
    │       └── xcschemes
    ├── MxiOS
    ├── MxmacOS
    └── mx.xcworkspace
        └── xcshareddata

Therefore I don’t know how to properly import it into pio.

I’ve tried using

import os

Import("env")

env.BuildSources(
    os.path.join("$BUILD_DIR", "dependencies","mx", "build"),
    os.path.join("$PROJECT_DIR", "dependencies", "mx","Sourcecode")
)

But it isn’t working (of course, I added the script to platformio.ini).

Thanks for the quick answer.

Okey, found out how to compile a .a file for my library. Can you expand what you’ve said about the env.BuildLibrary() or link me some documentation? I can’t find any.

Thanks.

If you have a .a file already, adding it into the build process is done by build_flags with -L<path to the folder where the .a file is> -l<library name without lib prefix>, e.g., -Lmx/lib -lmx (if the library is called libmx.a.

PlatformIO is a wrapper around SCons. env.BuildLibrary is a thin layer around the SCons functions for env.StaticLibrary

which is documented as SCons (Chapter 4. Building and Linking with Libraries).

Usage example see e.g. here.

1 Like

Thank you very much. Will try it this afternoon.

Added source files successfully with

-I$PROJECT_DIR/dependencies/mx/Sourcecode/include
-I$PROJECT_DIR/dependencies/mx/Sourcecode/private

They get added successfully to .vscode/c_cpp_properties.json:

{
    "configurations": [
        {
            "name": "PlatformIO",
            "includePath": [
                ...
                ".../dependencies/mx/Sourcecode/include",
                ".../dependencies/mx/Sourcecode/private",
                ...

However, it seems that the .cpp files are not being found, since I still get the undefined reference to error when trying to access the header files.

Also didn’t have any success adding the .a file. I don’t know what am I doing wrong, but the compiler seems to just ignore the -L and -l instructions.

Can you upload the full current project somewhere to download?

You can use the project task Avanced → Verbose Build to see the build commands and final linker invocation, if it’s not in there, something went wrong.

The source code is hosted on Github (ArnyminerZ/ElectronicMusicScore-Firmware).

I’ve seen in the library’s docs that it’s possible to include the library as Cmake (link). And it’s explained in the Espressiff’s docs how to do it. But I think that in Pio this might not be possible. Isn’t it?

Well okay, I just built libmx locally with CMake, but of course that was with gcc and not with xtensa32-elf-unknown-gcc, so that’s not good… I did get an error message of

c:/users/max/.platformio/packages/toolchain-xtensa-esp32@8.4.0+2021r2/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: libs\libmx.a: error adding symbols: file format not recognized
collect2.exe: error: ld returned 1 exit status
*** [.pio\build\esp32doit-devkit-v1\firmware.elf] Error 1

so the linker definitely tries to include the file, I just have to generate it correctly.

For ESP-IDF, PlatformIO calls into CMake, so if you can make this MX library a regular ESP-IDF component, PlatformIO should have no problems with it. But your current framework is arduino, so no CMake components are evaluated.

Do you have a precompiled version of libmx.a for Xtensa32?

Nope, I actually don’t understand what you are referring to :sweat_smile:. I’m quite used to working with Arduino, but C++ and Platformio are quite new for me.

Well your build script is doing cmake ... which on my computer detected the regular system compiler and will thus generate a binary for x86_64. Since you’re compiling for an ESP32, its CPU architecture is XTensa32, and so any precompiled library must be built for this architecture (through xtensa-esp32-elf-gcc and friends).

I’ve just added the XTensa32 compiler to my PATH (C:\Users\Max\.platformio\packages\toolchain-xtensa-esp32@8.4.0+2021r2\bin) and went into that dependencies/mx folder and executed

cmake … -G “Unix Makefiles” -DMX_BUILD_TESTS=off -DMX_BUILD_CORE_TESTS=off -DMX_BUILD_EXAMPLES=off -DCMAKE_SYSTEM_NAME=Generic -DCMAKE_SYSTEM_PROCESSOR=xtensa -DCMAKE_CROSSCOMPILING=1 -DCMAKE_C_COMPILER=xtensa-esp32-elf-gcc -DCMAKE_CXX_COMPILER=xtensa-esp32-elf-g++ -DCMAKE_AR=xtensa-esp32-elf-gcc-ar -DCMAKE_RANLIB=xtensa-esp32-elf-gcc-ranlib

And that generates Makefiles which seem to start building the library for XTensa32… with that file precompiled, the build should go through. Of course this doesn’t make PlatformIO build that library, but at least its a very quick test to get things going.

1 Like

Progress, I am able to build the libmx.a with a small quirk (copying ar and ranlib compiler tools into the build folder), but linking fails with ScoreReader.cpp:(.text+0x1542): dangerous relocation: call8: call target out of range: std::shared_ptr<mx::core::MovementTitle>::~shared_ptr() type errors, causes by not compiling the library with -mlongcalls. Will recompile with new flag…

1 Like

Wow that was a really hard ride. I had to recompile the library not only with -mlongcalls but also with -Os because of course, the library authors of libmx do not pass any default optimization flags, and the generated code with the implicit default -O0 is gargantuan. Still, the generated .a file is 25MByte big, but better than the previous 150MByte…

Additionally, the code of that library is so absurdly big, that just adding it into the build exceeds the default maximum firmware size of 1Mbyte, but it wants a whopping 2.6Mbytes. With the partition table that gives the biggest possible application size, huge_app.csv, that already fills up the available space by 85%…

Also, linking takes a nice ~60 seconds because of this gargantuan library. But in the end, it builds.

Linking .pio\build\esp32doit-devkit-v1\firmware.elf
Retrieving maximum program size .pio\build\esp32doit-devkit-v1\firmware.elf
Checking size .pio\build\esp32doit-devkit-v1\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [=         ]  13.7% (used 44756 bytes from 327680 bytes)
Flash: [========= ]  85.3% (used 2684073 bytes from 3145728 bytes)
Building .pio\build\esp32doit-devkit-v1\firmware.bin
esptool.py v3.2
Merged 25 ELF sections
============ [SUCCESS] Took 60.82 seconds ============

Following your last messages I added the compiler to my PATH, and ran

cmake . -G "Unix Makefiles" -DMX_BUILD_TESTS=off -DMX_BUILD_CORE_TESTS=off -DMX_BUILD_EXAMPLES=off -DCMAKE_SYSTEM_NAME=Generic -DCMAKE_SYSTEM_PROCESSOR=xtensa -DCMAKE_CROSSCOMPILING=1 -DCMAKE_C_COMPILER=xtensa-esp32-elf-gcc -DCMAKE_CXX_COMPILER=xtensa-esp32-elf-g++ -DCMAKE_AR=xtensa-esp32-elf-gcc-ar -DCMAKE_RANLIB=xtensa-esp32-elf-gcc-ranlib

from dependencies/mx and it worked fine.

Therefore, according to the last message should I simply add -mlongcalls and -Os to the end of the command? And then of course run make -j6 to build the .a file. Am I wrong?

In relation to the large size of the library, I thought it may give some issues, so if I can’t manage to fit the whole library and the code into the ESP32 I will try writing my own implementation, but just wanted to try if it worked out.

Really, thank you very much for the help.

For that, I had to edit the mx/CMakeLists.txt to add

// previous line
set(PUBLIC_DIR "${SOURCE_ROOT}/include")

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mlongcalls -Os")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mlongcalls -Os")

I will do a PR shortly.

1 Like

Please checkout the PR Add precompiled XTensa32 library, link into project, change partition table by maxgerhardt · Pull Request #3 · ArnyminerZ/ElectronicMusicScore-Firmware · GitHub and try to build.

I fear however that the moment you try to call into the libmx library, it will pull the corresponding code into the library which immediately makes it exceed the FLASH or (I-/D-)RAM limits of an ESP32.

Now there are remedies against that, such as using a board with 16Mbyte flash instead of 4MByte and a PSRAM chip for an additional 4MByte of RAM, but it will only get you so far.

Nice, I’m trying to build it right now.

I’ll have to make my own ESP32 module for this project, so I’ll consider adding that extra flash and RAM.

Again, thank you very much for the help, you’re awesome :rocket: