#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __EMSCRIPTEN__ #include #include #include #include extern "C" ssize_t readlink(const char* path, char* buffer, size_t size) { constexpr const char* executable = "/freecad/bin/freecad-naming-bridge"; if (std::strcmp(path, "/proc/self/exe") != 0) { return -1; } const size_t length = std::strlen(executable); if (size < length) { return -1; } std::memcpy(buffer, executable, length); return static_cast(length); } extern "C" char* realpath(const char* path, char* resolvedPath) { static constexpr const char* allowed[] = { "/freecad-user", "/freecad-user/config", "/freecad-user/data", "/freecad-user/cache", "/freecad-user/temp", }; if (!path || std::none_of(std::begin(allowed), std::end(allowed), [path](const char* entry) { return std::strcmp(path, entry) == 0; })) { return nullptr; } const size_t length = std::strlen(path); if (length >= PATH_MAX) { return nullptr; } char* output = resolvedPath ? resolvedPath : static_cast(std::malloc(length + 1)); if (!output) { return nullptr; } std::memcpy(output, path, length + 1); return output; } extern "C" int getpwuid_r(uid_t uid, struct passwd* password, char* buffer, size_t bufferSize, struct passwd** result) { constexpr const char identity[] = "freecad\0/freecad-user\0/bin/false"; if (bufferSize < sizeof(identity)) { *result = nullptr; return -1; } std::memcpy(buffer, identity, sizeof(identity)); password->pw_name = buffer; password->pw_passwd = const_cast(""); password->pw_uid = uid; password->pw_gid = getgid(); password->pw_gecos = buffer; password->pw_dir = buffer + sizeof("freecad"); password->pw_shell = password->pw_dir + sizeof("/freecad-user"); *result = password; return 0; } #endif namespace { constexpr int ABI_VERSION = 1; constexpr const char* FREECAD_VERSION = "1.1.1"; constexpr const char* FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"; void ensureApplication() { static bool initialized = false; if (initialized) { return; } char executable[] = "freecad-naming-bridge"; char console[] = "--console"; char* argv[] = {executable, console}; App::Application::init(2, argv); initialized = true; } QJsonObject requireObject(const QJsonObject& object, const char* key) { const QJsonValue value = object.value(key); if (!value.isObject()) { throw std::runtime_error(std::string("FreeCAD naming request requires object ") + key); } return value.toObject(); } QString requireString(const QJsonObject& object, const char* key) { const QJsonValue value = object.value(key); if (!value.isString() || value.toString().isEmpty()) { throw std::runtime_error(std::string("FreeCAD naming request requires string ") + key); } return value.toString(); } long requirePositiveTag(const QJsonObject& object, const char* key) { const double value = object.value(key).toDouble(-1.0); if (!std::isfinite(value) || value <= 0.0 || std::floor(value) != value || value > static_cast(0x7fffffff)) { throw std::runtime_error(std::string("FreeCAD naming request requires positive tag ") + key); } return static_cast(value); } int requireIndex(const QJsonObject& object, const char* key) { const double value = object.value(key).toDouble(-1.0); if (!std::isfinite(value) || value < 0.0 || std::floor(value) != value || value > static_cast(0x7ffffffe)) { throw std::runtime_error(std::string("FreeCAD naming request requires non-negative index ") + key); } return static_cast(value); } QString titleKind(const QString& value) { if (value == "face") { return "Face"; } if (value == "edge") { return "Edge"; } if (value == "vertex") { return "Vertex"; } throw std::runtime_error("FreeCAD naming history supports only face, edge, and vertex"); } QString canonicalOperation(const QString& operation) { QString output; output.reserve(operation.size()); for (const QChar character : operation) { if (character.isLetterOrNumber()) { output.append(character.toUpper()); } else if (character == '-') { output.append('_'); } else { throw std::runtime_error("FreeCAD naming operation contains an unsupported character"); } } if (output.isEmpty()) { throw std::runtime_error("FreeCAD naming operation is empty"); } return output; } QJsonObject parseNameToken(const std::string& raw) { if (raw.empty() || (raw[0] != ':' && raw[0] != '$' && raw[0] != ';')) { throw std::runtime_error("FreeCAD ElementMap emitted an invalid name token"); } std::vector parts; std::size_t start = 0; while (true) { const std::size_t dot = raw.find('.', start); parts.push_back(raw.substr(start, dot == std::string::npos ? dot : dot - start)); if (dot == std::string::npos) { break; } start = dot + 1; } const QString marker = QString(QChar(raw[0])); QJsonObject token {{"raw", QString::fromStdString(raw)}, {"marker", marker}}; QJsonArray suffix; if (raw[0] == ':') { if (parts.size() < 3) { throw std::runtime_error("FreeCAD ElementMap emitted a truncated indexed token"); } bool postfixOk = false; bool indexOk = false; const int postfixIndex = QString::fromStdString(parts[0].substr(1)).toInt(&postfixOk, 16); const int elementIndex = QString::fromStdString(parts[1]).toInt(&indexOk, 16); if (!postfixOk || !indexOk) { throw std::runtime_error("FreeCAD ElementMap emitted a malformed indexed token"); } token.insert("postfixIndex", postfixIndex); token.insert("elementIndex", elementIndex); for (std::size_t index = 2; index < parts.size(); ++index) { suffix.append(QString::fromStdString(parts[index])); } } else { token.insert("name", QString::fromStdString(parts[0].substr(1))); for (std::size_t index = 1; index < parts.size(); ++index) { suffix.append(QString::fromStdString(parts[index])); } } token.insert("suffix", suffix); return token; } QJsonObject parseElementMap(const std::string& saved) { std::istringstream stream(saved); unsigned rootId = 0; int postfixCount = 0; std::string label; if (!(stream >> rootId >> label >> postfixCount) || rootId == 0 || label != "PostfixCount" || postfixCount < 0) { throw std::runtime_error("FreeCAD ElementMap root header is invalid"); } QJsonArray postfixes; for (int index = 0; index < postfixCount; ++index) { std::string postfix; if (!(stream >> postfix)) { throw std::runtime_error("FreeCAD ElementMap postfix list is truncated"); } postfixes.append(QString::fromStdString(postfix)); } int mapCount = 0; if (!(stream >> label >> mapCount) || label != "MapCount" || mapCount <= 0) { throw std::runtime_error("FreeCAD ElementMap map count is invalid"); } QJsonArray maps; int rootMapIndex = 0; for (int mapOrdinal = 0; mapOrdinal < mapCount; ++mapOrdinal) { int mapIndex = 0; unsigned mapId = 0; int typeCount = 0; if (!(stream >> label >> mapIndex >> mapId >> typeCount) || label != "ElementMap" || mapIndex <= 0 || typeCount < 0) { throw std::runtime_error("FreeCAD ElementMap map header is invalid"); } rootMapIndex = std::max(rootMapIndex, mapIndex); QJsonArray sections; for (int typeOrdinal = 0; typeOrdinal < typeCount; ++typeOrdinal) { std::string sectionName; int childCount = 0; if (!(stream >> sectionName >> label >> childCount) || label != "ChildCount" || childCount != 0) { throw std::runtime_error("Candidate bridge does not accept child ElementMap records"); } int nameCount = 0; if (!(stream >> label >> nameCount) || label != "NameCount" || nameCount < 0) { throw std::runtime_error("FreeCAD ElementMap name count is invalid"); } QJsonArray names; for (int nameOrdinal = 0; nameOrdinal < nameCount; ++nameOrdinal) { QJsonArray tokens; std::string raw; std::string token; while (stream >> token) { if (token == "0") { break; } if (!raw.empty()) { raw += ' '; } raw += token; tokens.append(parseNameToken(token)); } if (!stream) { throw std::runtime_error("FreeCAD ElementMap name entry is truncated"); } names.append(QJsonObject { {"tokens", tokens}, {"trailing", "0"}, {"raw", QString::fromStdString(raw.empty() ? "0" : raw + " 0")}, }); } sections.append(QJsonObject { {"name", QString::fromStdString(sectionName)}, {"children", QJsonArray {}}, {"names", names}, }); } if (!(stream >> label) || label != "EndMap") { throw std::runtime_error("FreeCAD ElementMap map terminator is missing"); } maps.append(QJsonObject { {"index", mapIndex}, {"id", static_cast(mapId)}, {"typeCount", typeCount}, {"sections", sections}, }); } return QJsonObject { {"schemaVersion", 2}, {"nativeVersion", 1}, {"rootId", static_cast(rootId)}, {"postfixes", postfixes}, {"maps", maps}, {"rootMapIndex", rootMapIndex}, }; } QJsonObject stringHasherJson(const App::StringHasherRef& hasher) { QJsonArray entries; for (const auto& [id, reference] : hasher->getIDMap()) { const App::StringID& stringId = reference.deref(); int flags = 0; flags |= stringId.isBinary() ? 1 << 0 : 0; flags |= stringId.isHashed() ? 1 << 1 : 0; flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0; flags |= stringId.isPostfixed() ? 1 << 3 : 0; flags |= stringId.isIndexed() ? 1 << 4 : 0; flags |= stringId.isPrefixID() ? 1 << 5 : 0; flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0; flags |= stringId.isPersistent() ? 1 << 7 : 0; QJsonArray relatedIds; for (const auto& related : reference.relatedIDs()) { relatedIds.append(static_cast(related.value())); } entries.append(QJsonObject { {"id", static_cast(id)}, {"flags", flags}, {"relatedIds", relatedIds}, {"data", QString::fromUtf8(stringId.data())}, {"postfix", QString::fromUtf8(stringId.postfix())}, }); } return QJsonObject {{"schemaVersion", 2}, {"nativeVersion", 1}, {"entries", entries}}; } int stringIdFlags(const App::StringID& stringId) { int flags = 0; flags |= stringId.isBinary() ? 1 << 0 : 0; flags |= stringId.isHashed() ? 1 << 1 : 0; flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0; flags |= stringId.isPostfixed() ? 1 << 3 : 0; flags |= stringId.isIndexed() ? 1 << 4 : 0; flags |= stringId.isPrefixID() ? 1 << 5 : 0; flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0; flags |= stringId.isPersistent() ? 1 << 7 : 0; return flags; } long requireStringId(const QJsonValue& value, const char* label) { const double number = value.toDouble(-1.0); if (!std::isfinite(number) || number <= 0.0 || std::floor(number) != number || number > static_cast(0x7fffffff)) { throw std::runtime_error(std::string("Prior StringHasher requires positive ") + label); } return static_cast(number); } void restoreStringHasherTable(const QJsonObject& table, const App::StringHasherRef& hasher) { if (table.value("schemaVersion").toInt() != 2 || table.value("nativeVersion").toInt() != 1) { throw std::runtime_error("Prior StringHasher schema or native version is unsupported"); } const QJsonArray entries = table.value("entries").toArray(); if (entries.size() > 1000000) { throw std::runtime_error("Prior StringHasher entry count exceeds the candidate limit"); } long previousId = 0; for (const QJsonValue entryValue : entries) { if (!entryValue.isObject()) { throw std::runtime_error("Prior StringHasher entry must be an object"); } const QJsonObject entry = entryValue.toObject(); const long id = requireStringId(entry.value("id"), "entry ID"); if (id != previousId + 1) { throw std::runtime_error("Prior StringHasher IDs must be contiguous and ordered"); } const int flags = entry.value("flags").toInt(-1); constexpr int knownFlags = 0xff; if (flags < 0 || (flags & ~knownFlags) != 0) { throw std::runtime_error("Prior StringHasher entry contains unknown flags"); } if ((flags & ((1 << 0) | (1 << 1))) != 0) { throw std::runtime_error( "Candidate bridge cannot losslessly restore binary or one-way-hashed StringHasher entries"); } if (!entry.value("data").isString() || !entry.value("postfix").isString() || !entry.value("relatedIds").isArray()) { throw std::runtime_error("Prior StringHasher entry payload is incomplete"); } const QByteArray data = entry.value("data").toString().toUtf8(); const QByteArray postfix = entry.value("postfix").toString().toUtf8(); const QJsonArray relatedValues = entry.value("relatedIds").toArray(); std::vector relatedIds; relatedIds.reserve(relatedValues.size()); for (const QJsonValue relatedValue : relatedValues) { const long relatedId = requireStringId(relatedValue, "related ID"); if (relatedId >= id || !hasher->getID(relatedId)) { throw std::runtime_error("Prior StringHasher related ID is unresolved or forward-referenced"); } relatedIds.push_back(relatedId); } App::StringIDRef restored; if ((flags & (1 << 3)) == 0) { if (flags != 0 && flags != (1 << 7)) { throw std::runtime_error("Prior non-postfixed StringHasher flags cannot be reconstructed"); } restored = hasher->getID(data, App::StringHasher::Option::None); } else { QByteArray mappedData = data; if ((flags & ((1 << 4) | (1 << 6))) != 0) { mappedData += '1'; } Data::MappedName mapped(mappedData); if (!postfix.isEmpty()) { mapped += postfix; } int internalCount = 0; internalCount += (flags & (1 << 2)) != 0 ? 1 : 0; internalCount += (flags & (1 << 4)) != 0 ? 1 : 0; if (internalCount > static_cast(relatedIds.size())) { throw std::runtime_error("Prior StringHasher internal dependencies are truncated"); } Data::ElementIDRefs externalRefs; for (std::size_t index = static_cast(internalCount); index < relatedIds.size(); ++index) { externalRefs.push_back(hasher->getID(relatedIds[index])); } restored = hasher->getID(mapped, externalRefs); } if (!restored || restored.value() != id) { throw std::runtime_error("Prior StringHasher entry did not restore to its persistent ID"); } if ((flags & (1 << 7)) != 0) { restored.setPersistent(true); } const App::StringID& actual = restored.deref(); std::vector actualRelated; for (const App::StringIDRef& related : actual.relatedIDs()) { actualRelated.push_back(related.value()); } if (stringIdFlags(actual) != flags || actual.data() != data || actual.postfix() != postfix || actualRelated != relatedIds) { throw std::runtime_error("Prior StringHasher entry changed during strict restoration"); } previousId = id; } } void restorePriorStringHasher(const QJsonArray& inputs, const App::StringHasherRef& hasher) { QByteArray canonical; QJsonObject selected; for (const QJsonValue inputValue : inputs) { const QJsonObject evidence = inputValue.toObject().value("namingEvidence").toObject(); if (evidence.isEmpty()) { continue; } const QJsonValue tableValue = evidence.value("stringHasher"); if (!tableValue.isObject()) { const QJsonArray mappedNames = evidence.value("mappedNames").toArray(); const bool requiresHasher = std::any_of( mappedNames.begin(), mappedNames.end(), [](const QJsonValue& mappedValue) { const QJsonObject reference = mappedValue.toObject().value("reference").toObject(); return !reference.value("stringIds").toArray().isEmpty() || reference.contains("prefixStringId"); }); if (requiresHasher) { throw std::runtime_error("Prior mapped-name IDs require StringHasher evidence"); } continue; } const QJsonObject table = tableValue.toObject(); const QByteArray encoded = QJsonDocument(table).toJson(QJsonDocument::Compact); if (canonical.isEmpty()) { canonical = encoded; selected = table; } else if (canonical != encoded) { throw std::runtime_error("Candidate bridge cannot merge inconsistent prior StringHasher tables"); } } if (!selected.isEmpty()) { restoreStringHasherTable(selected, hasher); } } QJsonObject findSourceInput(const QJsonArray& inputs, const QString& source) { for (const QJsonValue value : inputs) { const QJsonObject input = value.toObject(); if (input.value("inputId").toString() == source || input.value("role").toString() == source) { return input; } } if (source == "object" && !inputs.isEmpty()) { return inputs.first().toObject(); } if (source == "tool" && inputs.size() > 1) { return inputs.at(1).toObject(); } throw std::runtime_error("FreeCAD naming history references an unknown input"); } struct SourceName { Data::MappedName name; QString persistentId; }; SourceName sourceNameFor(const QJsonObject& input, const QString& kind, int sourceIndex, const App::StringHasherRef& hasher) { const QString indexed = titleKind(kind) + QString::number(sourceIndex + 1); const QJsonValue evidenceValue = input.value("namingEvidence"); if (!evidenceValue.isObject()) { return {Data::MappedName(indexed.toStdString()), indexed}; } const QJsonArray mappedNames = evidenceValue.toObject().value("mappedNames").toArray(); for (const QJsonValue value : mappedNames) { const QJsonObject mapped = value.toObject(); if (mapped.value("kind").toString() != kind || mapped.value("resultIndex").toInt(-1) != sourceIndex) { continue; } const QJsonObject reference = requireObject(mapped, "reference"); const QJsonArray stringIds = reference.value("stringIds").toArray(); for (const QJsonValue idValue : stringIds) { if (!hasher->getID(requireStringId(idValue, "mapped-name string ID"))) { throw std::runtime_error("Prior mapped-name string ID is absent from StringHasher"); } } if (reference.contains("prefixStringId")) { const long prefixId = requireStringId(reference.value("prefixStringId"), "mapped-name prefix ID"); if (!hasher->getID(prefixId)) { throw std::runtime_error("Prior mapped-name prefix ID is absent from StringHasher"); } } Data::MappedName name(requireString(reference, "name").toStdString()); const QString postfix = reference.value("postfix").toString(); if (!postfix.isEmpty()) { name += postfix.toStdString(); } return {name, mapped.value("resultPersistentId").toString(indexed)}; } throw std::runtime_error("Prior naming evidence has no matching source subshape"); } QJsonObject mappedReference(const Data::MappedName& name, const Data::ElementIDRefs& stringIds) { QJsonArray ids; for (const App::StringIDRef& stringId : stringIds) { ids.append(static_cast(stringId.value())); } QJsonObject reference { {"name", QString::fromUtf8(name.dataBytes())}, {"postfix", QString::fromUtf8(name.postfixBytes())}, {"stringIds", ids}, }; const App::StringID::IndexID prefix = App::StringID::fromString(name.dataBytes()); if (prefix) { reference.insert("prefixStringId", static_cast(prefix.id)); } return reference; } std::string evidenceJsonImpl(const std::string& requestJson) { ensureApplication(); QJsonParseError parseError; const QJsonDocument document = QJsonDocument::fromJson(QByteArray::fromStdString(requestJson), &parseError); if (parseError.error != QJsonParseError::NoError || !document.isObject()) { throw std::runtime_error("FreeCAD naming request is not valid JSON"); } const QJsonObject request = document.object(); if (request.value("schemaVersion").toInt() != ABI_VERSION) { throw std::runtime_error("FreeCAD naming request schemaVersion is unsupported"); } const QString stageId = requireString(request, "stageId"); const QString resultObjectId = requireString(request, "resultObjectId"); const long resultTag = requirePositiveTag(request, "resultObjectTag"); const QString operation = requireString(request, "operation"); const QString operationPostfix = canonicalOperation(operation); const QJsonArray inputs = request.value("inputs").toArray(); const QJsonObject history = requireObject(request, "history"); const QJsonArray records = history.value("records").toArray(); if (inputs.isEmpty() || records.isEmpty()) { throw std::runtime_error("FreeCAD naming request requires inputs and native history records"); } App::StringHasherRef hasher(new App::StringHasher()); hasher->setSaveAll(true); restorePriorStringHasher(inputs, hasher); auto elementMap = std::make_shared(); elementMap->hasher = hasher; QJsonArray mappedNames; std::set resultKeys; std::map sourceOrdinals; for (const QJsonValue recordValue : records) { const QJsonObject record = recordValue.toObject(); const QString relation = requireString(record, "relation"); if (relation == "deleted") { continue; } if (relation != "modified" && relation != "generated") { throw std::runtime_error("FreeCAD naming history relation is unsupported"); } const QString sourceKind = requireString(record, "kind"); const QString resultKind = record.value("resultKind").toString(sourceKind); titleKind(sourceKind); const QString resultType = titleKind(resultKind); const int sourceIndex = requireIndex(record, "sourceIndex"); const QString source = record.value("sourceId").toString( requireString(record, "source")); const QJsonObject input = findSourceInput(inputs, source); const QString sourceObjectId = requireString(input, "objectId"); const long sourceTag = requirePositiveTag(input, "objectTag"); const SourceName sourceName = sourceNameFor(input, sourceKind, sourceIndex, hasher); QJsonArray resultIndexes = record.value("resultIndexes").toArray(); if (resultIndexes.isEmpty() && record.contains("resultIndex")) { resultIndexes.append(record.value("resultIndex")); } if (resultIndexes.isEmpty()) { throw std::runtime_error("Non-deleted FreeCAD naming history requires result indexes"); } const std::string ordinalKey = (sourceObjectId + '|' + sourceKind + '|' + QString::number(sourceIndex) + '|' + relation) .toStdString(); for (const QJsonValue resultValue : resultIndexes) { const int resultIndex = requireIndex(QJsonObject {{"value", resultValue}}, "value"); const std::string resultKey = (resultKind + ':' + QString::number(resultIndex)).toStdString(); if (!resultKeys.insert(resultKey).second) { throw std::runtime_error("FreeCAD naming history maps the same result subshape twice"); } const int ordinal = ++sourceOrdinals[ordinalKey]; std::ostringstream postfix; postfix << (relation == "modified" ? Data::POSTFIX_MOD : Data::POSTFIX_GEN); if (ordinal > 1) { postfix << ordinal; } Data::MappedName encoded(sourceName.name); Data::ElementIDRefs stringIds; elementMap->encodeElementName( resultType.at(0).toLatin1(), encoded, postfix, &stringIds, resultTag, operationPostfix.toUtf8().constData(), sourceTag); const Data::IndexedName resultIndexed( (resultType + QString::number(resultIndex + 1)).toUtf8()); const Data::MappedName stored = elementMap->setElementName(resultIndexed, encoded, resultTag, &stringIds); if (!stored) { throw std::runtime_error("FreeCAD ElementMap rejected a native history mapping"); } mappedNames.append(QJsonObject { {"kind", resultKind}, {"resultIndex", resultIndex}, {"resultPersistentId", resultType + QString::number(resultIndex + 1)}, {"relation", relation}, {"reference", mappedReference(stored, stringIds)}, {"sourceRefs", QJsonArray {QJsonObject { {"objectId", sourceObjectId}, {"persistentId", sourceName.persistentId}, {"stageId", input.value("stageId")}, }}}, }); } } if (mappedNames.isEmpty()) { throw std::runtime_error("FreeCAD naming history has no result subshape evidence"); } elementMap->beforeSave(hasher); std::ostringstream saved; elementMap->save(saved); const QJsonObject response { {"schemaVersion", ABI_VERSION}, {"stageId", stageId}, {"resultObjectId", resultObjectId}, {"status", "native-evidence"}, {"mappedNames", mappedNames}, {"stringHasher", stringHasherJson(hasher)}, {"elementMap2", parseElementMap(saved.str())}, }; return QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString(); } std::string evidenceJson(const std::string& requestJson) { try { return evidenceJsonImpl(requestJson); } catch (const Base::Exception& exception) { return QJsonDocument(QJsonObject { {"schemaVersion", ABI_VERSION}, {"status", "error"}, {"error", QString::fromStdString("FreeCAD private naming failure: " + exception.getMessage())}, }).toJson(QJsonDocument::Compact).toStdString(); } catch (const std::exception& exception) { return QJsonDocument(QJsonObject { {"schemaVersion", ABI_VERSION}, {"status", "error"}, {"error", QString::fromUtf8(exception.what())}, }).toJson(QJsonDocument::Compact).toStdString(); } catch (...) { return R"({"schemaVersion":1,"status":"error","error":"Unknown native exception"})"; } } int freecadNamingAbiVersion() { return ABI_VERSION; } std::string freecadNamingCapabilitiesJson() { return QJsonDocument(QJsonObject { {"schemaVersion", ABI_VERSION}, {"freecadVersion", FREECAD_VERSION}, {"sourceCommit", FREECAD_COMMIT}, {"mappedNameRef", true}, {"stringHasher", true}, {"elementMap2", true}, {"operations", QJsonArray { "fuse", "cut", "common", "rotate", "pad", "pocket", "loft", "pipe", "revolution", "groove", "fillet", "chamfer", "hole", "draft", "thickness", "linear-pattern", "polar-pattern", "mirrored", "multi-transform", }}, {"maxRequestBytes", 16 * 1024 * 1024}, {"maxResponseBytes", 32 * 1024 * 1024}, }).toJson(QJsonDocument::Compact).toStdString(); } } // namespace EMSCRIPTEN_BINDINGS(freecad_private_naming_bridge) { emscripten::function("freecadNamingAbiVersion", &freecadNamingAbiVersion); emscripten::function("freecadNamingCapabilitiesJson", &freecadNamingCapabilitiesJson); emscripten::function("freecadNamingEvidenceJson", &evidenceJson); }