Skip to content

Commit bc7d499

Browse files
Add C++ codebase generator and transpiler tool
Co-Authored-By: zemanadam893@gmail.com <zemanadam893@gmail.com>
1 parent 60d5272 commit bc7d499

12 files changed

Lines changed: 355 additions & 7 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ option(LIBSCRATCHCPP_NETWORK_SUPPORT "Support for downloading projects" ON)
1111
option(LIBSCRATCHCPP_PRINT_LLVM_IR "Print LLVM IR of compiled Scratch scripts (for debugging)" OFF)
1212
option(LIBSCRATCHCPP_ENABLE_CODE_ANALYZER "Analyze Scratch scripts to enable various optimizations" ON)
1313
option(LIBSCRATCHCPP_LLVM_INTEGER_SUPPORT "Use integers when possible to enable various optimizations" ON)
14+
option(LIBSCRATCHCPP_BUILD_TRANSPILER "Build the Scratch to C++ transpiler tool" ON)
1415
option(LIBSCRATCHCPP_ENABLE_SANITIZER "Enable sanitizer to detect memory issues" OFF)
1516

1617
if (LIBSCRATCHCPP_ENABLE_SANITIZER)
@@ -153,6 +154,11 @@ target_compile_definitions(scratchcpp PUBLIC LIBSCRATCHCPP_VERSION_MAJOR=${PROJE
153154
target_compile_definitions(scratchcpp PUBLIC LIBSCRATCHCPP_VERSION_MINOR=${PROJECT_VERSION_MINOR})
154155
target_compile_definitions(scratchcpp PUBLIC LIBSCRATCHCPP_VERSION_PATCH=${PROJECT_VERSION_PATCH})
155156

157+
# Scratch to C++ transpiler
158+
if (LIBSCRATCHCPP_BUILD_TRANSPILER)
159+
add_subdirectory(transpiler)
160+
endif()
161+
156162
# Unit tests
157163
target_link_libraries(scratchcpp PRIVATE scratchcpp-test-export)
158164

‎src/engine/internal/cpp/CMakeLists.txt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ target_sources(scratchcpp
88
cppcodebuilderfactory.cpp
99
cppcodebuilderfactory.h
1010
cppregister.h
11-
cppcompilercontext.cpp
11+
cppcodebasegenerator.cpp
12+
cppcodebasegenerator.h
13+
cppcompilercontext.cpp
1214
cppcompilercontext.h
1315
cppexecutablecode.cpp
1416
cppexecutablecode.h
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
#include <scratchcpp/project.h>
4+
#include <filesystem>
5+
#include <fstream>
6+
#include <sstream>
7+
#include <iostream>
8+
#include <iomanip>
9+
#include <vector>
10+
11+
#include "cppcodebasegenerator.h"
12+
#include "cppcodebuilderfactory.h"
13+
#include "cppcompilercontext.h"
14+
#include "../../compiler_p.h"
15+
16+
using namespace libscratchcpp;
17+
18+
namespace
19+
{
20+
21+
// Creates C++ code builders and keeps the compiler contexts to collect the generated code
22+
class RecordingFactory : public ICodeBuilderFactory
23+
{
24+
public:
25+
std::shared_ptr<ICodeBuilder> create(CompilerContext *ctx, BlockPrototype *procedurePrototype, Compiler::CodeType codeType) const override
26+
{
27+
return CppCodeBuilderFactory::instance()->create(ctx, procedurePrototype, codeType);
28+
}
29+
30+
std::shared_ptr<CompilerContext> createCtx(IEngine *engine, Target *target) const override
31+
{
32+
auto ctx = CppCodeBuilderFactory::instance()->createCtx(engine, target);
33+
contexts.push_back(ctx);
34+
return ctx;
35+
}
36+
37+
mutable std::vector<std::shared_ptr<CompilerContext>> contexts;
38+
};
39+
40+
bool writeFile(const std::filesystem::path &path, const std::string &content)
41+
{
42+
std::ofstream file(path, std::ios::binary);
43+
44+
if (!file.is_open()) {
45+
std::cerr << "error: failed to write " << path.string() << std::endl;
46+
return false;
47+
}
48+
49+
file << content;
50+
return true;
51+
}
52+
53+
} // namespace
54+
55+
/*!
56+
* Compiles the given project using the C++ backend and writes a CMake project to outputDir:
57+
* - project.cpp - the generated code of all scripts
58+
* - project_data.cpp - the embedded project file
59+
* - main.cpp - loads and runs the embedded project
60+
* - CMakeLists.txt
61+
*/
62+
bool CppCodebaseGenerator::generate(const std::string &projectFile, const std::string &outputDir, const std::string &projectName)
63+
{
64+
std::ifstream input(projectFile, std::ios::binary);
65+
66+
if (!input.is_open()) {
67+
std::cerr << "error: failed to read " << projectFile << std::endl;
68+
return false;
69+
}
70+
71+
std::stringstream data;
72+
data << input.rdbuf();
73+
74+
RecordingFactory factory;
75+
ICodeBuilderFactory *oldFactory = CompilerPrivate::builderFactory();
76+
CompilerPrivate::setBuilderFactory(&factory);
77+
78+
std::stringstream code;
79+
bool ret;
80+
81+
{
82+
Project project(projectFile);
83+
ret = project.load();
84+
85+
if (ret) {
86+
code << "// Generated by libscratchcpp, do not edit\n\n" << CppCompilerContext::prelude() << "\n";
87+
88+
for (auto ctx : factory.contexts)
89+
code << "\n" << static_cast<CppCompilerContext *>(ctx.get())->moduleSource();
90+
}
91+
}
92+
93+
CompilerPrivate::setBuilderFactory(oldFactory);
94+
95+
if (!ret)
96+
return false;
97+
98+
std::filesystem::path dir(outputDir);
99+
std::error_code err;
100+
std::filesystem::create_directories(dir, err);
101+
102+
return writeFile(dir / "project.cpp", code.str()) && writeFile(dir / "project_data.cpp", dataSource(data.str())) && writeFile(dir / "main.cpp", mainSource()) &&
103+
writeFile(dir / "CMakeLists.txt", cmakeSource(projectName));
104+
}
105+
106+
std::string CppCodebaseGenerator::mainSource()
107+
{
108+
return R"(// Generated by libscratchcpp, do not edit
109+
110+
#include <scratchcpp/project.h>
111+
#include <filesystem>
112+
#include <fstream>
113+
#include <string>
114+
115+
extern const unsigned char scratchcpp_project_data[];
116+
extern const unsigned long long scratchcpp_project_size;
117+
118+
extern "C" void cpp_use_cpp_backend();
119+
120+
int main(int argc, char **argv)
121+
{
122+
// libscratchcpp loads projects from files, so the embedded project is extracted first
123+
std::filesystem::path path = std::filesystem::temp_directory_path() / ("scratchcpp_project_" + std::to_string(std::hash<std::string>()(argv[0])) + ".sb3");
124+
125+
{
126+
std::ofstream file(path, std::ios::binary);
127+
file.write(reinterpret_cast<const char *>(scratchcpp_project_data), scratchcpp_project_size);
128+
}
129+
130+
// Scripts are compiled by the C++ backend which uses the code in project.cpp
131+
cpp_use_cpp_backend();
132+
133+
libscratchcpp::Project project(path.string());
134+
bool loaded = project.load();
135+
std::error_code err;
136+
std::filesystem::remove(path, err);
137+
138+
if (!loaded)
139+
return 1;
140+
141+
project.run();
142+
return 0;
143+
}
144+
)";
145+
}
146+
147+
std::string CppCodebaseGenerator::dataSource(const std::string &data)
148+
{
149+
std::stringstream ss;
150+
ss << "// Generated by libscratchcpp, do not edit\n\n";
151+
ss << "extern const unsigned long long scratchcpp_project_size = " << data.size() << "ULL;\n\n";
152+
ss << "extern const unsigned char scratchcpp_project_data[] = {";
153+
154+
for (size_t i = 0; i < data.size(); i++) {
155+
if (i % 16 == 0)
156+
ss << "\n ";
157+
158+
ss << " 0x" << std::hex << std::setw(2) << std::setfill('0') << (int)(unsigned char)data[i] << ",";
159+
}
160+
161+
ss << std::dec << "\n 0x00\n};\n";
162+
return ss.str();
163+
}
164+
165+
std::string CppCodebaseGenerator::cmakeSource(const std::string &projectName)
166+
{
167+
std::string ret = R"(# Generated by libscratchcpp
168+
cmake_minimum_required(VERSION 3.14)
169+
project(@NAME@ LANGUAGES CXX)
170+
171+
set(CMAKE_CXX_STANDARD 17)
172+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
173+
174+
# Use either libscratchcpp sources (LIBSCRATCHCPP_SOURCE_DIR) or a prebuilt library (LIBSCRATCHCPP_INCLUDE_DIR and LIBSCRATCHCPP_LIBRARY)
175+
set(LIBSCRATCHCPP_SOURCE_DIR "" CACHE PATH "libscratchcpp source directory")
176+
set(LIBSCRATCHCPP_INCLUDE_DIR "" CACHE PATH "libscratchcpp include directory")
177+
set(LIBSCRATCHCPP_LIBRARY "" CACHE FILEPATH "libscratchcpp library")
178+
179+
add_executable(@NAME@ main.cpp project.cpp project_data.cpp)
180+
181+
if(LIBSCRATCHCPP_SOURCE_DIR)
182+
set(LIBSCRATCHCPP_BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE)
183+
add_subdirectory(${LIBSCRATCHCPP_SOURCE_DIR} libscratchcpp)
184+
target_link_libraries(@NAME@ PRIVATE scratchcpp)
185+
elseif(LIBSCRATCHCPP_INCLUDE_DIR AND LIBSCRATCHCPP_LIBRARY)
186+
target_include_directories(@NAME@ PRIVATE ${LIBSCRATCHCPP_INCLUDE_DIR})
187+
target_link_libraries(@NAME@ PRIVATE ${LIBSCRATCHCPP_LIBRARY})
188+
else()
189+
message(FATAL_ERROR "Set LIBSCRATCHCPP_SOURCE_DIR or LIBSCRATCHCPP_INCLUDE_DIR and LIBSCRATCHCPP_LIBRARY")
190+
endif()
191+
)";
192+
std::string name = projectName;
193+
194+
for (char &c : name) {
195+
if (!std::isalnum((unsigned char)c) && c != '_' && c != '-')
196+
c = '_';
197+
}
198+
199+
size_t pos;
200+
201+
while ((pos = ret.find("@NAME@")) != std::string::npos)
202+
ret.replace(pos, 6, name);
203+
204+
return ret;
205+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
#pragma once
4+
5+
#include <string>
6+
7+
#include "../../../test_export.h"
8+
9+
namespace libscratchcpp
10+
{
11+
12+
/*! Generates a standalone C++ codebase (CMake project) from a Scratch project. */
13+
class LIBSCRATCHCPP_TEST_EXPORT CppCodebaseGenerator
14+
{
15+
public:
16+
static bool generate(const std::string &projectFile, const std::string &outputDir, const std::string &projectName = "project");
17+
18+
static std::string mainSource();
19+
static std::string dataSource(const std::string &data);
20+
static std::string cmakeSource(const std::string &projectName);
21+
};
22+
23+
} // namespace libscratchcpp

‎src/engine/internal/cpp/cppcodebuilder.cpp‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include <scratchcpp/list.h>
77
#include <scratchcpp/target.h>
88
#include <scratchcpp/value.h>
9+
#include <scratchcpp/iengine.h>
10+
#include <scratchcpp/stage.h>
911
#include <iomanip>
1012
#include <cmath>
1113
#include <cassert>
@@ -161,8 +163,13 @@ CompilerConstant *CppCodeBuilder::addConstValue(const Value &value)
161163
break;
162164

163165
case ValueType::Pointer: {
164-
// NOTE: Pointers are only valid in the current process
165166
type = Compiler::StaticType::Pointer;
167+
expr = entityPtr(value.toPointer());
168+
169+
if (!expr.empty())
170+
break;
171+
172+
// NOTE: Other pointers are only valid in the current process
166173
std::stringstream ss;
167174
ss << "reinterpret_cast<void *>(0x" << std::hex << reinterpret_cast<uintptr_t>(value.toPointer()) << ")";
168175
expr = ss.str();
@@ -1009,6 +1016,32 @@ const std::string &CppCodeBuilder::variablePtr(Variable *variable)
10091016
return m_variables[variable] = name;
10101017
}
10111018

1019+
std::string CppCodeBuilder::entityPtr(const void *ptr)
1020+
{
1021+
// Lists and variables are resolved at runtime, so generated code doesn't depend on their addresses
1022+
std::vector<Target *> targets;
1023+
1024+
if (m_ctx->target())
1025+
targets.push_back(m_ctx->target());
1026+
1027+
if (m_ctx->engine() && m_ctx->engine()->stage())
1028+
targets.push_back(m_ctx->engine()->stage());
1029+
1030+
for (Target *target : targets) {
1031+
for (auto list : target->lists()) {
1032+
if (list.get() == ptr)
1033+
return "static_cast<void *>(" + listPtr(list.get()) + ")";
1034+
}
1035+
1036+
for (auto var : target->variables()) {
1037+
if (var.get() == ptr)
1038+
return "static_cast<void *>(" + variablePtr(var.get()) + ")";
1039+
}
1040+
}
1041+
1042+
return "";
1043+
}
1044+
10121045
const std::string &CppCodeBuilder::listPtr(List *list)
10131046
{
10141047
auto it = m_lists.find(list);

‎src/engine/internal/cpp/cppcodebuilder.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ class LIBSCRATCHCPP_TEST_EXPORT CppCodeBuilder : public ICodeBuilder
164164

165165
const std::string &variablePtr(Variable *variable);
166166
const std::string &listPtr(List *list);
167+
std::string entityPtr(const void *ptr);
167168

168169
CppCompilerContext *m_ctx = nullptr;
169170
BlockPrototype *m_procedurePrototype = nullptr;

‎src/engine/internal/cpp/cppruntime.cpp‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
#include <cassert>
55

66
#include "cppruntime.h"
7+
#include "cppcodebuilderfactory.h"
8+
#include "cppcodebasegenerator.h"
9+
#include "../../compiler_p.h"
710
#include "cppexecutioncontext.h"
811

912
using namespace libscratchcpp;
@@ -46,3 +49,16 @@ extern "C"
4649
return static_cast<CppExecutionContext *>(ctx)->finished();
4750
}
4851
}
52+
53+
extern "C"
54+
{
55+
LIBSCRATCHCPP_EXPORT void cpp_use_cpp_backend()
56+
{
57+
CompilerPrivate::setBuilderFactory(CppCodeBuilderFactory::instance().get());
58+
}
59+
60+
LIBSCRATCHCPP_EXPORT bool cpp_generate_codebase(const char *projectFile, const char *outputDir, const char *projectName)
61+
{
62+
return CppCodebaseGenerator::generate(projectFile, outputDir, projectName);
63+
}
64+
}

‎src/engine/internal/cpp/cppruntime.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ extern "C"
1818
LIBSCRATCHCPP_EXPORT void cpp_register_function(uint64_t moduleKey, const char *name, void *function);
1919
LIBSCRATCHCPP_EXPORT void cpp_mark_thread_as_finished(ExecutionContext *ctx);
2020
LIBSCRATCHCPP_EXPORT bool cpp_is_thread_finished(ExecutionContext *ctx);
21+
22+
// Makes the compiler use the C++ backend (used by generated codebases)
23+
LIBSCRATCHCPP_EXPORT void cpp_use_cpp_backend();
24+
25+
// Generates a standalone C++ codebase from a Scratch project
26+
LIBSCRATCHCPP_EXPORT bool cpp_generate_codebase(const char *projectFile, const char *outputDir, const char *projectName);
2127
}
2228

2329
} // namespace libscratchcpp

‎test/cpp/cases.h‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ struct CppTestCase
3838
{
3939
std::string name;
4040
std::function<std::shared_ptr<ExecutableCode>(CppTestEnv &)> build;
41-
std::string expectedOutput; // standard output after running the code
42-
int expectedRuns = -1; // the number of run() calls until the script finishes (-1 to skip)
43-
std::string expectedValue; // the return value of reporters and hat predicates
41+
std::string expectedOutput; // standard output after running the code
42+
int expectedRuns = -1; // the number of run() calls until the script finishes (-1 to skip)
43+
std::string expectedValue; // the return value of reporters and hat predicates
4444
Compiler::CodeType codeType = Compiler::CodeType::Script;
4545
};
4646

0 commit comments

Comments
 (0)