Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src: use Blob{Des|S}erializer for SEA blobs #47962

Merged
merged 6 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions src/blob_serializer_deserializer-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,28 @@ std::vector<T> BlobDeserializer<Impl>::ReadVector() {

template <typename Impl>
std::string BlobDeserializer<Impl>::ReadString() {
std::string_view view = ReadStringView();
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
std::string result(view.data(),
view.size()); // This creates a copy of view.data.
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
return result;
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
}

template <typename Impl>
std::string_view BlobDeserializer<Impl>::ReadStringView() {
size_t length = ReadArithmetic<size_t>();

if (is_debug) {
Debug("ReadString(), length=%d: ", length);
Debug("ReadStringView(), length=%d: ", length);
}

CHECK_GT(length, 0); // There should be no empty strings.
MallocedBuffer<char> buf(length + 1);
memcpy(buf.data, sink.data() + read_total, length + 1);
std::string result(buf.data, length); // This creates a copy of buf.data.
std::string_view result(sink.data() + read_total, length);

if (is_debug) {
Debug("\"%s\", read %zu bytes\n", result.c_str(), length + 1);
Debug("\"%s\", read %zu bytes\n", result.data(), result.size());
}

read_total += length + 1;
read_total += length;
return result;
}

Expand Down Expand Up @@ -262,17 +268,15 @@ size_t BlobSerializer<Impl>::WriteVector(const std::vector<T>& data) {
// [ 4/8 bytes ] length
// [ |length| bytes ] contents
template <typename Impl>
size_t BlobSerializer<Impl>::WriteString(const std::string& data) {
size_t BlobSerializer<Impl>::WriteStringView(const std::string_view& data) {
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
CHECK_GT(data.size(), 0); // No empty strings should be written.
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
size_t written_total = WriteArithmetic<size_t>(data.size());
if (is_debug) {
std::string str = ToStr(data);
Debug("WriteString(), length=%zu: \"%s\"\n", data.size(), data.c_str());
Debug("WriteStringView(), length=%zu: %p\n", data.size(), data.data());
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
}

// Write the null-terminated string.
size_t length = data.size() + 1;
sink.insert(sink.end(), data.c_str(), data.c_str() + length);
size_t length = data.size();
sink.insert(sink.end(), data.data(), data.data() + length);
written_total += length;

if (is_debug) {
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -282,6 +286,14 @@ size_t BlobSerializer<Impl>::WriteString(const std::string& data) {
return written_total;
}

template <typename Impl>
size_t BlobSerializer<Impl>::WriteString(const std::string& data) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
size_t BlobSerializer<Impl>::WriteString(const std::string& data) {
size_t BlobSerializer<Impl>::WriteString(const std::string_view data) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the method that writes strings (which logs the string directly in debug mode). The one that doesn't is WriteStringView (which obviously doesn't log that data as string).

if (is_debug) {
Debug("WriteString(), length=%zu: \"%s\"\n", data.size(), data.data());
}
return WriteStringView(data);
}

// Helper for writing an array of numeric types.
template <typename Impl>
template <typename T>
Expand Down
8 changes: 3 additions & 5 deletions src/blob_serializer_deserializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class BlobDeserializer : public BlobSerializerDeserializer {
std::vector<T> ReadVector();

std::string ReadString();
std::string_view ReadStringView();

// Helper for reading an array of numeric types.
template <typename T>
Expand All @@ -77,11 +78,7 @@ template <typename Impl>
class BlobSerializer : public BlobSerializerDeserializer {
public:
explicit BlobSerializer(bool is_debug_v)
: BlobSerializerDeserializer(is_debug_v) {
// Currently the snapshot blob built with an empty script is around 4MB.
// So use that as the default sink size.
sink.reserve(4 * 1024 * 1024);
}
: BlobSerializerDeserializer(is_debug_v) {}
~BlobSerializer() {}

Impl* impl() { return static_cast<Impl*>(this); }
Expand All @@ -102,6 +99,7 @@ class BlobSerializer : public BlobSerializerDeserializer {
// The layout of a written string:
// [ 4/8 bytes ] length
// [ |length| bytes ] contents
size_t WriteStringView(const std::string_view& data);
size_t WriteString(const std::string& data);

// Helper for writing an array of numeric types.
Expand Down
1 change: 1 addition & 0 deletions src/debug_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str);
V(INSPECTOR_PROFILER) \
V(CODE_CACHE) \
V(NGTCP2_DEBUG) \
V(SEA) \
V(WASI) \
V(MKSNAPSHOT)

Expand Down
10 changes: 6 additions & 4 deletions src/node_main_instance.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,16 @@ ExitCode NodeMainInstance::Run() {

void NodeMainInstance::Run(ExitCode* exit_code, Environment* env) {
if (*exit_code == ExitCode::kNoFailure) {
bool is_sea = false;
bool runs_sea_code = false;
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
if (sea::IsSingleExecutable()) {
is_sea = true;
LoadEnvironment(env, sea::FindSingleExecutableCode());
runs_sea_code = true;
sea::SeaResource sea = sea::FindSingleExecutableResource();
std::string_view code = sea.code;
LoadEnvironment(env, code);
}
#endif
if (!is_sea) {
if (!runs_sea_code) {
LoadEnvironment(env, StartExecutionCallback{});
}

Expand Down
141 changes: 87 additions & 54 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "node_sea.h"

#include "blob_serializer_deserializer-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "json_parser.h"
Expand Down Expand Up @@ -34,16 +35,6 @@ namespace node {
namespace sea {

namespace {
// A special number that will appear at the beginning of the single executable
// preparation blobs ready to be injected into the binary. We use this to check
// that the data given to us are intended for building single executable
// applications.
const uint32_t kMagic = 0x143da20;

enum class SeaFlags : uint32_t {
kDefault = 0,
kDisableExperimentalSeaWarning = 1 << 0,
};

SeaFlags operator|(SeaFlags x, SeaFlags y) {
return static_cast<SeaFlags>(static_cast<uint32_t>(x) |
Expand All @@ -59,47 +50,93 @@ SeaFlags operator|=(/* NOLINT (runtime/references) */ SeaFlags& x, SeaFlags y) {
return x = x | y;
}

struct SeaResource {
SeaFlags flags = SeaFlags::kDefault;
std::string_view code;
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
class SeaSerializer : public BlobSerializer<SeaSerializer> {
public:
SeaSerializer() : BlobSerializer<SeaSerializer>(false) {}
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved

template <typename T,
std::enable_if_t<!std::is_same<T, std::string>::value>* = nullptr,
std::enable_if_t<!std::is_arithmetic<T>::value>* = nullptr>
size_t Write(const T& data);
};

SeaResource FindSingleExecutableResource() {
template <>
size_t SeaSerializer::Write(const SeaResource& sea) {
sink.reserve(SeaResource::kHeaderSize + sea.code.size());

size_t written_total = WriteArithmetic<uint32_t>(kMagic);
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
written_total += WriteArithmetic<uint32_t>(static_cast<uint32_t>(sea.flags));
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
DCHECK_EQ(written_total, SeaResource::kHeaderSize);

written_total += WriteStringView(sea.code);
return written_total;
}

class SeaDeserializer : public BlobDeserializer<SeaDeserializer> {
public:
explicit SeaDeserializer(std::string_view v)
: BlobDeserializer<SeaDeserializer>(false, v) {}

template <typename T,
std::enable_if_t<!std::is_same<T, std::string>::value>* = nullptr,
std::enable_if_t<!std::is_arithmetic<T>::value>* = nullptr>
T Read();
};

template <>
SeaResource SeaDeserializer::Read() {
uint32_t magic = ReadArithmetic<uint32_t>();
CHECK_EQ(magic, kMagic);
SeaFlags flags(static_cast<SeaFlags>(ReadArithmetic<uint32_t>()));
per_process::Debug(DebugCategory::SEA,
"Read SEA flag %" PRIu32 "\n",
static_cast<uint32_t>(flags));
CHECK_EQ(read_total, SeaResource::kHeaderSize);

std::string_view code = ReadStringView();
per_process::Debug(DebugCategory::SEA,
"Read SEA resource code %p %zu\n",
code.data(),
code.size());
return {flags, code};
}

std::string_view FindSingleExecutableBlob() {
CHECK(IsSingleExecutable());
static const SeaResource sea_resource = []() -> SeaResource {
static const std::string_view result = []() -> std::string_view {
size_t size;
#ifdef __APPLE__
postject_options options;
postject_options_init(&options);
options.macho_segment_name = "NODE_SEA";
const char* code = static_cast<const char*>(
const char* blob = static_cast<const char*>(
postject_find_resource("NODE_SEA_BLOB", &size, &options));
#else
const char* code = static_cast<const char*>(
const char* blob = static_cast<const char*>(
postject_find_resource("NODE_SEA_BLOB", &size, nullptr));
#endif
uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0];
CHECK_EQ(first_word, kMagic);
SeaFlags flags{
reinterpret_cast<const SeaFlags*>(code + sizeof(first_word))[0]};
// TODO(joyeecheung): do more checks here e.g. matching the versions.
return {
flags,
{
code + SeaResource::kHeaderSize,
size - SeaResource::kHeaderSize,
},
};
return {blob, size};
}();
return sea_resource;
per_process::Debug(DebugCategory::SEA,
"Found SEA resource %p %zu\n",
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
result.data(),
result.size());
return result;
}

} // namespace
} // anonymous namespace

std::string_view FindSingleExecutableCode() {
SeaResource sea_resource = FindSingleExecutableResource();
return sea_resource.code;
SeaResource FindSingleExecutableResource() {
static const SeaResource sea_resource = []() -> SeaResource {
std::string_view blob = FindSingleExecutableBlob();
per_process::Debug(DebugCategory::SEA,
"Found SEA resource %p %zu\n",
blob.data(),
blob.size());
SeaDeserializer r(blob);
return r.Read<SeaResource>();
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
}();
return sea_resource;
}

bool IsSingleExecutable() {
Expand Down Expand Up @@ -194,51 +231,47 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
return result;
}

bool GenerateSingleExecutableBlob(const SeaConfig& config) {
ExitCode GenerateSingleExecutableBlob(const SeaConfig& config) {
std::string main_script;
// TODO(joyeecheung): unify the file utils.
int r = ReadFileSync(&main_script, config.main_path.c_str());
if (r != 0) {
const char* err = uv_strerror(r);
FPrintF(stderr, "Cannot read main script %s:%s\n", config.main_path, err);
return false;
return ExitCode::kGenericUserError;
}

std::vector<char> sink;
// TODO(joyeecheung): reuse the SnapshotSerializerDeserializer for this.
sink.reserve(SeaResource::kHeaderSize + main_script.size());
const char* pos = reinterpret_cast<const char*>(&kMagic);
sink.insert(sink.end(), pos, pos + sizeof(kMagic));
pos = reinterpret_cast<const char*>(&(config.flags));
sink.insert(sink.end(), pos, pos + sizeof(SeaFlags));
sink.insert(
sink.end(), main_script.data(), main_script.data() + main_script.size());

uv_buf_t buf = uv_buf_init(sink.data(), sink.size());
SeaResource sea{config.flags,
std::string_view{main_script.data(), main_script.size()}};
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved

SeaSerializer serializer;
serializer.Write(sea);

uv_buf_t buf = uv_buf_init(serializer.sink.data(), serializer.sink.size());
r = WriteFileSync(config.output_path.c_str(), buf);
if (r != 0) {
const char* err = uv_strerror(r);
FPrintF(stderr, "Cannot write output to %s:%s\n", config.output_path, err);
return false;
return ExitCode::kGenericUserError;
}

FPrintF(stderr,
"Wrote single executable preparation blob to %s\n",
config.output_path);
return true;
return ExitCode::kNoFailure;
}

} // anonymous namespace

ExitCode BuildSingleExecutableBlob(const std::string& config_path) {
std::optional<SeaConfig> config_opt =
ParseSingleExecutableConfig(config_path);
if (!config_opt.has_value() ||
!GenerateSingleExecutableBlob(config_opt.value())) {
return ExitCode::kGenericUserError;
if (config_opt.has_value()) {
ExitCode code = GenerateSingleExecutableBlob(config_opt.value());
return code;
}

return ExitCode::kNoFailure;
return ExitCode::kGenericUserError;
}

void Initialize(Local<Object> target,
Expand Down
19 changes: 18 additions & 1 deletion src/node_sea.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,26 @@

namespace node {
namespace sea {
// A special number that will appear at the beginning of the single executable
// preparation blobs ready to be injected into the binary. We use this to check
// that the data given to us are intended for building single executable
// applications.
const uint32_t kMagic = 0x143da20;

enum class SeaFlags : uint32_t {
kDefault = 0,
kDisableExperimentalSeaWarning = 1 << 0,
};

struct SeaResource {
SeaFlags flags = SeaFlags::kDefault;
std::string_view code;

static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
};

bool IsSingleExecutable();
std::string_view FindSingleExecutableCode();
SeaResource FindSingleExecutableResource();
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv);
node::ExitCode BuildSingleExecutableBlob(const std::string& config_path);
} // namespace sea
Expand Down
6 changes: 5 additions & 1 deletion src/node_snapshotable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,11 @@ class SnapshotSerializer : public BlobSerializer<SnapshotSerializer> {
SnapshotSerializer()
: BlobSerializer<SnapshotSerializer>(
per_process::enabled_debug_list.enabled(
DebugCategory::MKSNAPSHOT)) {}
DebugCategory::MKSNAPSHOT)) {
// Currently the snapshot blob built with an empty script is around 4MB.
// So use that as the default sink size.
sink.reserve(4 * 1024 * 1024);
}

template <typename T,
std::enable_if_t<!std::is_same<T, std::string>::value>* = nullptr,
Expand Down