The rustmaker provides comprehensive support for converting UNO type definitions into idiomatic Rust code with proper memory safety and type system integration. Also adds the RustFile, RustOptions, and RustProducer classes to handle Rust code generation, file management, and command line option parsing for various UNO type kinds such as enums, structs, and interfaces. Change-Id: Iaf87af13352c55e81c1745ef7a8f591c3e61e0a4 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/188088 Reviewed-by: Stephan Bergmann <stephan.bergmann@collabora.com> Tested-by: Jenkins
665 lines
20 KiB
C++
665 lines
20 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
|
|
/*
|
|
* This file is part of the LibreOffice project.
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
// Implementation of Rust file generation and module management
|
|
|
|
#include "rustfile.hxx"
|
|
#include <algorithm>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <vector>
|
|
#include <functional>
|
|
#include <codemaker/global.hxx>
|
|
|
|
RustFile::RustFile(std::string_view directory, std::string_view typeName)
|
|
: m_filePath(createFilePath(directory, typeName))
|
|
, m_indentLevel(0)
|
|
{
|
|
}
|
|
|
|
std::string RustFile::getPath() const { return m_filePath.string(); }
|
|
|
|
void RustFile::openFile()
|
|
{
|
|
std::filesystem::create_directories(m_filePath.parent_path());
|
|
createModFiles();
|
|
addFileToModDeclaration();
|
|
m_fileStream.open(m_filePath, std::fstream::out | std::fstream::trunc);
|
|
m_indentLevel = 0;
|
|
}
|
|
|
|
void RustFile::closeFile() { m_fileStream.close(); }
|
|
|
|
void RustFile::createLibFile(const std::filesystem::path& outputDir)
|
|
{
|
|
// Create generated/rustmaker directory first, then src directory inside it
|
|
std::filesystem::path generatedDir = outputDir / "generated";
|
|
std::filesystem::path rustmakerDir = generatedDir / "rustmaker";
|
|
std::filesystem::path srcDir = rustmakerDir / "src";
|
|
std::filesystem::create_directories(srcDir);
|
|
|
|
std::filesystem::path libFile = srcDir / "lib.rs";
|
|
if (!std::filesystem::exists(libFile))
|
|
{
|
|
std::ofstream libStream(libFile);
|
|
libStream << "//! Generated Rust bindings for LibreOffice UNO types\n";
|
|
libStream << "//! \n";
|
|
libStream << "//! This crate contains automatically generated Rust bindings\n";
|
|
libStream << "//! for LibreOffice UNO (Universal Network Objects) types.\n\n";
|
|
libStream.close();
|
|
}
|
|
}
|
|
|
|
void RustFile::createCargoFile(const std::filesystem::path& outputDir, const std::string& crateName)
|
|
{
|
|
// Create Cargo.toml in the generated/rustmaker directory
|
|
std::filesystem::path generatedDir = outputDir / "generated";
|
|
std::filesystem::path rustmakerDir = generatedDir / "rustmaker";
|
|
std::filesystem::path cargoFile = rustmakerDir / "Cargo.toml";
|
|
if (!std::filesystem::exists(cargoFile))
|
|
{
|
|
std::ofstream cargoStream(cargoFile);
|
|
cargoStream << "[package]\n";
|
|
cargoStream << "name = \"" << crateName << "\"\n";
|
|
cargoStream << "version = \"0.1.0\"\n";
|
|
cargoStream << "edition = \"2024\"\n";
|
|
cargoStream << "\n";
|
|
cargoStream << "[lib]\n";
|
|
cargoStream << "name = \"rustmaker\"\n";
|
|
// Support both dynamic and static linking
|
|
cargoStream << "crate-type = [\"cdylib\", \"rlib\"]\n";
|
|
cargoStream << "description = \"Generated Rust bindings for LibreOffice UNO types\"\n\n";
|
|
cargoStream << "[dependencies]\n";
|
|
cargoStream << "# UNO runtime dependencies\n";
|
|
cargoStream << "rust_uno = { path = \"../../../../rust_uno\" }\n";
|
|
cargoStream.close();
|
|
}
|
|
}
|
|
|
|
void RustFile::createGeneratedModFile(const std::filesystem::path& outputDir)
|
|
{
|
|
// Create generated directory and its mod.rs file
|
|
std::filesystem::path generatedDir = outputDir / "generated";
|
|
std::filesystem::create_directories(generatedDir);
|
|
|
|
std::filesystem::path modFile = generatedDir / "mod.rs";
|
|
if (!std::filesystem::exists(modFile))
|
|
{
|
|
std::ofstream modStream(modFile);
|
|
modStream << "//! Generated opaque bindings for LibreOffice UNO types\n";
|
|
modStream << "//!\n";
|
|
modStream << "//! This module contains both Rust and C++ opaque bindings\n";
|
|
modStream << "//! auto-generated by rustmaker\n\n";
|
|
modStream << "// Include the generated rustmaker bindings directly\n";
|
|
modStream << "// The rustmaker directory contains a complete Rust project structure\n";
|
|
modStream << "// We include its lib.rs as a module here\n";
|
|
modStream << "#[path = \"rustmaker/src/lib.rs\"]\n";
|
|
modStream << "pub mod rustmaker;\n\n";
|
|
modStream << "// Re-export the UNO namespace for easier access\n";
|
|
modStream << "pub use rustmaker::com;\n\n";
|
|
modStream << "// Note: cpp_rustmaker directory contains C++ bridge files\n";
|
|
modStream << "// These are compiled separately and linked via build.rs\n";
|
|
modStream.close();
|
|
}
|
|
}
|
|
|
|
void RustFile::finalizeModFiles(const std::filesystem::path& outputDir)
|
|
{
|
|
// Find all directories in the rustmaker source tree and update their mod.rs files
|
|
std::filesystem::path rustmakerSrc = outputDir / "generated" / "rustmaker" / "src";
|
|
|
|
if (!std::filesystem::exists(rustmakerSrc))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Start processing from the rustmaker src directory
|
|
processDirectoryRecursively(rustmakerSrc);
|
|
}
|
|
|
|
RustFile& RustFile::beginBlock()
|
|
{
|
|
beginLine();
|
|
append("{");
|
|
endLine();
|
|
++m_indentLevel;
|
|
return *this;
|
|
}
|
|
|
|
RustFile& RustFile::endBlock()
|
|
{
|
|
--m_indentLevel;
|
|
beginLine();
|
|
append("}");
|
|
endLine();
|
|
return *this;
|
|
}
|
|
|
|
// Text output methods
|
|
RustFile& RustFile::beginLine()
|
|
{
|
|
for (int i = 0; i < m_indentLevel; i++)
|
|
{
|
|
m_fileStream << " ";
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
RustFile& RustFile::extraIndent()
|
|
{
|
|
m_fileStream << " ";
|
|
return *this;
|
|
}
|
|
|
|
RustFile& RustFile::append(std::string_view item)
|
|
{
|
|
m_fileStream << item;
|
|
return *this;
|
|
}
|
|
|
|
RustFile& RustFile::append(std::u16string_view item)
|
|
{
|
|
m_fileStream << u2b(item);
|
|
return *this;
|
|
}
|
|
|
|
RustFile& RustFile::endLine()
|
|
{
|
|
m_fileStream << '\n';
|
|
return *this;
|
|
}
|
|
|
|
void RustFile::createModFiles()
|
|
{
|
|
std::filesystem::path outputRoot = findOutputRoot();
|
|
|
|
// Create lib and cargo files only once at the proper root level
|
|
// outputRoot should be the rustmaker directory, so we pass its parent to the create functions
|
|
// which will then create the rustmaker directory and put files inside it
|
|
createGeneratedModFile(
|
|
outputRoot.parent_path().parent_path()); // Create generated/mod.rs at top level
|
|
createLibFile(
|
|
outputRoot.parent_path().parent_path()); // Create in the directory containing generated/
|
|
createCargoFile(outputRoot.parent_path().parent_path(), "rustmaker");
|
|
|
|
// Build path hierarchy from file location up to project root
|
|
std::filesystem::path workingPath = m_filePath.parent_path();
|
|
std::vector<std::filesystem::path> pathsToProcess;
|
|
|
|
// Collect all directory paths from current file up to root
|
|
while (workingPath != outputRoot && workingPath.has_parent_path())
|
|
{
|
|
pathsToProcess.push_back(workingPath);
|
|
workingPath = workingPath.parent_path();
|
|
}
|
|
|
|
// Process paths from root down to ensure proper mod.rs structure
|
|
std::reverse(pathsToProcess.begin(), pathsToProcess.end());
|
|
for (const auto& path : pathsToProcess)
|
|
{
|
|
ensureModFileExists(path);
|
|
}
|
|
}
|
|
|
|
// Complex path resolution - walks up directory tree to find project root
|
|
std::filesystem::path RustFile::findOutputRoot()
|
|
{
|
|
std::filesystem::path currentPath = m_filePath.parent_path();
|
|
|
|
// Walk up until we find a directory structure indicating project root
|
|
while (currentPath.has_parent_path() && currentPath != currentPath.parent_path())
|
|
{
|
|
if (currentPath.filename() == "src")
|
|
{
|
|
return currentPath.parent_path(); // Return rustmaker directory
|
|
}
|
|
|
|
// Look for rustmaker directory as the project root
|
|
if (currentPath.filename() == "rustmaker")
|
|
{
|
|
return currentPath;
|
|
}
|
|
|
|
// Look for existing src/ subdirectory as root indicator
|
|
std::filesystem::path srcDir = currentPath / "src";
|
|
if (std::filesystem::exists(srcDir))
|
|
{
|
|
return currentPath;
|
|
}
|
|
|
|
currentPath = currentPath.parent_path();
|
|
}
|
|
|
|
// Fallback: find parent of src directory (which should be rustmaker)
|
|
std::filesystem::path result = m_filePath.parent_path();
|
|
while (result.has_parent_path() && result.filename() != "src")
|
|
{
|
|
result = result.parent_path();
|
|
}
|
|
|
|
if (result.filename() == "src")
|
|
{
|
|
return result.parent_path(); // Return rustmaker directory
|
|
}
|
|
|
|
return result; // Default case
|
|
}
|
|
|
|
void RustFile::addFileToModDeclaration()
|
|
{
|
|
std::filesystem::path parentDir = m_filePath.parent_path();
|
|
std::filesystem::path modFile = parentDir / "mod.rs";
|
|
std::string fileName = m_filePath.stem().string(); // Get filename without extension
|
|
|
|
if (std::filesystem::exists(modFile))
|
|
{
|
|
addModuleDeclaration(modFile, fileName);
|
|
}
|
|
}
|
|
|
|
void RustFile::ensureModFileExists(const std::filesystem::path& dirPath)
|
|
{
|
|
// Skip mod.rs creation for the src directory itself
|
|
if (dirPath.filename() == "src")
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::filesystem::path modFile = dirPath / "mod.rs";
|
|
if (!std::filesystem::exists(modFile))
|
|
{
|
|
// Create mod.rs file with proper structure
|
|
std::ofstream modStream(modFile);
|
|
modStream << "// Generated mod.rs file\n\n";
|
|
modStream << "#[allow(non_snake_case)]\n";
|
|
modStream.close();
|
|
}
|
|
|
|
// After ensuring the mod.rs exists, collect all subdirectories and modules
|
|
// to automatically add pub use ::* patterns
|
|
std::vector<std::string> childModules;
|
|
|
|
// Scan directory for subdirectories (modules) and .rs files
|
|
if (std::filesystem::exists(dirPath))
|
|
{
|
|
try
|
|
{
|
|
for (const auto& entry : std::filesystem::directory_iterator(dirPath))
|
|
{
|
|
if (entry.is_directory())
|
|
{
|
|
std::string dirName = entry.path().filename().string();
|
|
childModules.push_back(dirName);
|
|
}
|
|
else if (entry.is_regular_file() && entry.path().extension() == ".rs")
|
|
{
|
|
std::string fileName = entry.path().stem().string();
|
|
if (fileName != "mod") // Don't include mod.rs itself
|
|
{
|
|
childModules.push_back(fileName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (const std::filesystem::filesystem_error&)
|
|
{
|
|
// Handle permission errors gracefully
|
|
}
|
|
}
|
|
|
|
// Add module declarations and pub use statements for all child modules
|
|
for (const std::string& moduleName : childModules)
|
|
{
|
|
addModuleDeclaration(modFile, moduleName);
|
|
}
|
|
|
|
// Recursively ensure parent modules are properly declared
|
|
if (dirPath.has_parent_path())
|
|
{
|
|
std::filesystem::path parentDir = dirPath.parent_path();
|
|
std::filesystem::path outputRoot = findOutputRoot();
|
|
|
|
if (parentDir >= outputRoot)
|
|
{
|
|
std::string moduleName = dirPath.filename().string();
|
|
|
|
if (parentDir.filename() == "src")
|
|
{
|
|
std::filesystem::path libFile = parentDir / "lib.rs";
|
|
if (std::filesystem::exists(libFile))
|
|
{
|
|
addModuleDeclaration(libFile, moduleName);
|
|
}
|
|
}
|
|
else if (parentDir == outputRoot)
|
|
{
|
|
std::filesystem::path libFile = parentDir / "src" / "lib.rs";
|
|
if (std::filesystem::exists(libFile))
|
|
{
|
|
addModuleDeclaration(libFile, moduleName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ensureModFileExists(parentDir);
|
|
std::filesystem::path parentModFile = parentDir / "mod.rs";
|
|
if (std::filesystem::exists(parentModFile))
|
|
{
|
|
addModuleDeclaration(parentModFile, moduleName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RustFile::addModuleDeclaration(const std::filesystem::path& modFile,
|
|
const std::string& moduleName)
|
|
{
|
|
// Read existing content to check for duplicates and organize sections
|
|
std::ifstream inFile(modFile);
|
|
std::string line;
|
|
std::vector<std::string> pubModLines;
|
|
std::vector<std::string> otherLines;
|
|
bool foundModDeclaration = false;
|
|
|
|
while (std::getline(inFile, line))
|
|
{
|
|
if (line.find("pub mod " + moduleName + ";") != std::string::npos)
|
|
{
|
|
foundModDeclaration = true;
|
|
}
|
|
|
|
if (line.find("pub mod ") == 0)
|
|
{
|
|
pubModLines.push_back(line);
|
|
}
|
|
else if (!(line.find("pub use ") == 0 && line.find("::*;") != std::string::npos))
|
|
{
|
|
// Skip old pub use re-export lines, include everything else
|
|
otherLines.push_back(line);
|
|
}
|
|
}
|
|
inFile.close();
|
|
|
|
// If module declaration is already declared, return
|
|
if (foundModDeclaration)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Add new declarations if not found
|
|
if (!foundModDeclaration)
|
|
{
|
|
pubModLines.push_back("pub mod " + moduleName + ";");
|
|
}
|
|
// Re-export generation removed - causes unused import warnings
|
|
|
|
// Rewrite the file with organized structure
|
|
std::ofstream outFile(modFile, std::ios::trunc);
|
|
|
|
// Write other lines first (comments, etc.)
|
|
for (const auto& otherLine : otherLines)
|
|
{
|
|
// Skip re-export comment lines
|
|
if (otherLine.find("// Re-export all items from child modules") == std::string::npos)
|
|
{
|
|
outFile << otherLine << "\n";
|
|
}
|
|
}
|
|
|
|
// Write all pub mod declarations
|
|
for (const auto& modLine : pubModLines)
|
|
{
|
|
outFile << modLine << "\n";
|
|
}
|
|
|
|
// Re-export section removed - causes unused import warnings
|
|
|
|
outFile.close();
|
|
}
|
|
|
|
// Converts UNO type name to file path (dots become path separators)
|
|
std::filesystem::path RustFile::createFilePath(std::string_view dir, std::string_view type)
|
|
{
|
|
std::string subdir(type);
|
|
for (char& c : subdir)
|
|
if (c == '.')
|
|
c = '/';
|
|
|
|
std::filesystem::path path(dir);
|
|
path /= "rustmaker"; // Add rustmaker subdirectory for generated types
|
|
path /= "src";
|
|
path /= subdir + ".rs";
|
|
return path;
|
|
}
|
|
|
|
// CppFile implementation
|
|
CppFile::CppFile(std::string_view directory, std::string_view typeName)
|
|
: m_filePath(createCppFilePath(directory, typeName))
|
|
, m_indentLevel(0)
|
|
{
|
|
}
|
|
|
|
CppFile::CppFile(std::string_view directory, std::string_view typeName, std::string_view extension)
|
|
: m_filePath(createCppFilePath(directory, typeName, extension))
|
|
, m_indentLevel(0)
|
|
{
|
|
}
|
|
|
|
std::string CppFile::getPathString() const { return m_filePath.string(); }
|
|
std::filesystem::path CppFile::getPath() const { return m_filePath; }
|
|
std::string CppFile::getExtension() const { return m_filePath.extension().string(); }
|
|
|
|
void CppFile::openFile()
|
|
{
|
|
std::filesystem::create_directories(m_filePath.parent_path());
|
|
m_fileStream.open(m_filePath, std::fstream::out | std::fstream::trunc);
|
|
m_indentLevel = 0;
|
|
}
|
|
|
|
void CppFile::openFileAppend()
|
|
{
|
|
std::filesystem::create_directories(m_filePath.parent_path());
|
|
m_fileStream.open(m_filePath, std::fstream::out | std::fstream::app);
|
|
m_indentLevel = 0;
|
|
}
|
|
|
|
void CppFile::closeFile() { m_fileStream.close(); }
|
|
|
|
CppFile& CppFile::beginBlock()
|
|
{
|
|
beginLine();
|
|
append("{");
|
|
endLine();
|
|
++m_indentLevel;
|
|
return *this;
|
|
}
|
|
|
|
CppFile& CppFile::endBlock()
|
|
{
|
|
--m_indentLevel;
|
|
beginLine();
|
|
append("}");
|
|
endLine();
|
|
return *this;
|
|
}
|
|
|
|
CppFile& CppFile::beginLine()
|
|
{
|
|
m_fileStream << std::string(4 * m_indentLevel, ' ');
|
|
return *this;
|
|
}
|
|
|
|
CppFile& CppFile::extraIndent()
|
|
{
|
|
m_fileStream << " ";
|
|
return *this;
|
|
}
|
|
|
|
CppFile& CppFile::append(std::string_view item)
|
|
{
|
|
m_fileStream << item;
|
|
return *this;
|
|
}
|
|
|
|
CppFile& CppFile::append(std::u16string_view item)
|
|
{
|
|
m_fileStream << u2b(item);
|
|
return *this;
|
|
}
|
|
|
|
CppFile& CppFile::endLine()
|
|
{
|
|
m_fileStream << std::endl;
|
|
return *this;
|
|
}
|
|
|
|
CppFile& CppFile::writeIncludes(const std::vector<std::string>& includes)
|
|
{
|
|
for (const auto& include : includes)
|
|
{
|
|
beginLine().append("#include ").append(include).endLine();
|
|
}
|
|
endLine();
|
|
return *this;
|
|
}
|
|
|
|
CppFile& CppFile::writeNamespaceBegin(const std::vector<std::string>& namespaces)
|
|
{
|
|
for (const auto& ns : namespaces)
|
|
{
|
|
beginLine().append("namespace ").append(ns).append(" {").endLine();
|
|
}
|
|
endLine();
|
|
return *this;
|
|
}
|
|
|
|
CppFile& CppFile::writeNamespaceEnd(const std::vector<std::string>& namespaces)
|
|
{
|
|
endLine();
|
|
for (auto it = namespaces.rbegin(); it != namespaces.rend(); ++it)
|
|
{
|
|
beginLine().append("} // namespace ").append(*it).endLine();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
std::filesystem::path CppFile::createCppFilePath(std::string_view dir, std::string_view type,
|
|
std::string_view extension)
|
|
{
|
|
std::filesystem::path outputDir(dir);
|
|
|
|
// For the special case of "rust_uno_bindings", create it directly in the output directory
|
|
if (type == "rust_uno_bindings")
|
|
{
|
|
return outputDir / (std::string(type) + std::string(extension));
|
|
}
|
|
|
|
std::filesystem::path basePath = outputDir;
|
|
|
|
std::string typeName(type);
|
|
|
|
// Extract interface name (part after last dot)
|
|
size_t lastDot = typeName.find_last_of('.');
|
|
std::string interfaceName
|
|
= (lastDot != std::string::npos) ? typeName.substr(lastDot + 1) : typeName;
|
|
|
|
// Create namespace directory path from the full type name
|
|
if (lastDot != std::string::npos)
|
|
{
|
|
// Convert namespace dots to path separators: com.sun.star.frame -> com/sun/star/frame/
|
|
std::string namespacePath = typeName.substr(0, lastDot);
|
|
|
|
// Replace dots with path separators
|
|
std::replace(namespacePath.begin(), namespacePath.end(), '.', '/');
|
|
|
|
// Build full path: cpp_rustmaker/com/sun/star/frame/XComponentLoader.hxx
|
|
return basePath / namespacePath / (interfaceName + std::string(extension));
|
|
}
|
|
else
|
|
{
|
|
// No namespace, put directly in cpp_rustmaker directory
|
|
return basePath / (interfaceName + std::string(extension));
|
|
}
|
|
}
|
|
|
|
void RustFile::processDirectoryRecursively(const std::filesystem::path& currentDir)
|
|
{
|
|
if (!std::filesystem::exists(currentDir) || !std::filesystem::is_directory(currentDir))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Skip the root src directory
|
|
if (currentDir.filename() == "src" && currentDir.parent_path().filename() == "rustmaker")
|
|
{
|
|
// Process subdirectories but don't create mod.rs for src itself
|
|
for (const auto& entry : std::filesystem::directory_iterator(currentDir))
|
|
{
|
|
if (entry.is_directory())
|
|
{
|
|
processDirectoryRecursively(entry.path());
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
std::vector<std::string> childModules;
|
|
|
|
try
|
|
{
|
|
// Collect all subdirectories and .rs files
|
|
for (const auto& entry : std::filesystem::directory_iterator(currentDir))
|
|
{
|
|
if (entry.is_directory())
|
|
{
|
|
std::string dirName = entry.path().filename().string();
|
|
childModules.push_back(dirName);
|
|
// Recursively process subdirectory
|
|
processDirectoryRecursively(entry.path());
|
|
}
|
|
else if (entry.is_regular_file() && entry.path().extension() == ".rs")
|
|
{
|
|
std::string fileName = entry.path().stem().string();
|
|
if (fileName != "mod" && fileName != "lib") // Don't include mod.rs or lib.rs
|
|
{
|
|
childModules.push_back(fileName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (const std::filesystem::filesystem_error&)
|
|
{
|
|
// Handle permission errors gracefully
|
|
return;
|
|
}
|
|
|
|
// Create/update mod.rs file if there are child modules
|
|
if (!childModules.empty())
|
|
{
|
|
std::filesystem::path modFile = currentDir / "mod.rs";
|
|
|
|
// Create the mod.rs file with all declarations
|
|
std::ofstream modStream(modFile, std::ios::trunc);
|
|
modStream << "// Generated mod.rs file\n\n";
|
|
modStream << "#[allow(non_snake_case)]\n";
|
|
|
|
for (const std::string& moduleName : childModules)
|
|
{
|
|
modStream << "pub mod " << moduleName << ";\n";
|
|
}
|
|
|
|
// Re-export section removed per request
|
|
|
|
modStream.close();
|
|
}
|
|
}
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|