Files
odfstudio/codemaker/source/rustmaker/rustfile.cxx
T
Mohamed Ali d9f05dc007 Rust Bindings: Add rustmaker (Rust codemaker)
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
2025-09-11 21:38:59 +02:00

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: */