Skip to content

Commit eb703f7

Browse files
committed
src: support snapshot in single executable applications
This patch adds snapshot support to single executable applications. To build a snapshot from the main script when preparing the blob that will be injected into the single executable application, add `"useSnapshot": true` to the configuration passed to `--experimental-sea-config`. For example: ``` { "main": "snapshot.js", "output": "sea-prep.blob", "useSnapshot": true } ``` The main script used to build the snapshot must invoke `v8.startupSnapshot.setDeserializeMainFunction()` to configure the entry point. The generated startup snapshot would be part of the preparation blob and get injected into the final executable. When the single executable application is launched, instead of running the `main` script from scratch, Node.js would instead deserialize the snapshot to get to the state initialized during build-time directly.
1 parent 3ce303c commit eb703f7

9 files changed

+265
-41
lines changed

doc/api/single-executable-applications.md

+27-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
added:
77
- v19.7.0
88
- v18.16.0
9+
changes:
10+
- version: REPLACEME
11+
pr-url: https://github.com/nodejs/node/pull/46824
12+
description: Added support for "useSnapshot".
913
-->
1014

1115
> Stability: 1 - Experimental: This feature is being designed and will change.
@@ -177,14 +181,33 @@ The configuration currently reads the following top-level fields:
177181
{
178182
"main": "/path/to/bundled/script.js",
179183
"output": "/path/to/write/the/generated/blob.blob",
180-
"disableExperimentalSEAWarning": true // Default: false
184+
"disableExperimentalSEAWarning": true, // Default: false
185+
"useSnapshot": true // Default: false
181186
}
182187
```
183188
184189
If the paths are not absolute, Node.js will use the path relative to the
185190
current working directory. The version of the Node.js binary used to produce
186191
the blob must be the same as the one to which the blob will be injected.
187192
193+
### Startup snapshot support
194+
195+
When `useSnapshot` is set to true in the configuration, during the generation
196+
of the single executable preparation blob, Node.js will run the `main` script
197+
to generate a startup snapshot. The script must invoke
198+
[`v8.startupSnapshot.setDeserializeMainFunction()`][] to set up the entry point.
199+
The generated startup snapshot would be part of the preparation blob and get
200+
injected into the final executable. When the single executable application is
201+
launched, instead of running the `main` script from scratch, Node.js would
202+
instead deserialize the snapshot to get to the state initialized during
203+
build-time directly.
204+
205+
The general constraints of the startup snapshot scripts also apply to the main
206+
script when it's used to build snapshot for the single executable application,
207+
and the main script can use the [`v8.startupSnapshot` API][] to adapt to
208+
these constraints. See
209+
[documentation about startup snapshot support in Node.js][].
210+
188211
## Notes
189212

190213
### `require(id)` in the injected module is not file based
@@ -257,6 +280,9 @@ to help us document them.
257280
[`process.execPath`]: process.md#processexecpath
258281
[`require()`]: modules.md#requireid
259282
[`require.main`]: modules.md#accessing-the-main-module
283+
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
284+
[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api
285+
[documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot
260286
[fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses
261287
[postject]: https://github.com/nodejs/postject
262288
[signtool]: https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool

lib/internal/process/pre_execution.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ function patchProcessObject(expandArgv1) {
143143
__proto__: null,
144144
enumerable: true,
145145
// Only set it to true during snapshot building.
146-
configurable: getOptionValue('--build-snapshot'),
146+
configurable: isBuildingSnapshot(),
147147
value: process.argv[0],
148148
});
149149

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ struct SnapshotData {
533533

534534
void ToFile(FILE* out) const;
535535
std::vector<char> ToBlob() const;
536+
void ToBlob(std::vector<char>* out) const;
536537
// If returns false, the metadata doesn't match the current Node.js binary,
537538
// and the caller should not consume the snapshot data.
538539
bool Check() const;

src/node.cc

+60-23
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,17 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
292292

293293
CHECK(!env->isolate_data()->is_building_snapshot());
294294

295+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
296+
if (sea::IsSingleExecutable()) {
297+
sea::SeaResource sea = sea::FindSingleExecutableResource();
298+
// The SEA preparation blob building process should already enforce this,
299+
// this check is just here to guard against the unlikely case where
300+
// the SEA preparation blob has been manually modified by someone.
301+
CHECK_IMPLIES(sea.use_snapshot(),
302+
!env->snapshot_deserialize_main().IsEmpty());
303+
}
304+
#endif
305+
295306
// TODO(joyeecheung): move these conditions into JS land and let the
296307
// deserialize main function take precedence. For workers, we need to
297308
// move the pre-execution part into a different file that can be
@@ -1198,49 +1209,66 @@ ExitCode GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr,
11981209
return exit_code;
11991210
}
12001211

1201-
ExitCode LoadSnapshotDataAndRun(const SnapshotData** snapshot_data_ptr,
1202-
const InitializationResultImpl* result) {
1203-
ExitCode exit_code = result->exit_code_enum();
1212+
bool LoadSnapshotData(const SnapshotData** snapshot_data_ptr) {
12041213
// nullptr indicates there's no snapshot data.
12051214
DCHECK_NULL(*snapshot_data_ptr);
1215+
1216+
bool is_sea = false;
1217+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
1218+
if (sea::IsSingleExecutable()) {
1219+
is_sea = true;
1220+
sea::SeaResource sea = sea::FindSingleExecutableResource();
1221+
if (sea.use_snapshot()) {
1222+
std::unique_ptr<SnapshotData> read_data =
1223+
std::make_unique<SnapshotData>();
1224+
std::string_view snapshot = sea.main_code_or_snapshot;
1225+
if (SnapshotData::FromBlob(read_data.get(), snapshot)) {
1226+
*snapshot_data_ptr = read_data.release();
1227+
return true;
1228+
} else {
1229+
fprintf(stderr, "Invalid snapshot data in single executable binary\n");
1230+
return false;
1231+
}
1232+
}
1233+
}
1234+
#endif
1235+
12061236
// --snapshot-blob indicates that we are reading a customized snapshot.
1207-
if (!per_process::cli_options->snapshot_blob.empty()) {
1237+
// Ignore it when we are loading from SEA.
1238+
if (!is_sea && !per_process::cli_options->snapshot_blob.empty()) {
12081239
std::string filename = per_process::cli_options->snapshot_blob;
12091240
FILE* fp = fopen(filename.c_str(), "rb");
12101241
if (fp == nullptr) {
12111242
fprintf(stderr, "Cannot open %s", filename.c_str());
1212-
exit_code = ExitCode::kStartupSnapshotFailure;
1213-
return exit_code;
1243+
return false;
12141244
}
12151245
std::unique_ptr<SnapshotData> read_data = std::make_unique<SnapshotData>();
12161246
bool ok = SnapshotData::FromFile(read_data.get(), fp);
12171247
fclose(fp);
12181248
if (!ok) {
1219-
// If we fail to read the customized snapshot,
1220-
// simply exit with kStartupSnapshotFailure.
1221-
exit_code = ExitCode::kStartupSnapshotFailure;
1222-
return exit_code;
1249+
return false;
12231250
}
12241251
*snapshot_data_ptr = read_data.release();
1225-
} else if (per_process::cli_options->node_snapshot) {
1226-
// If --snapshot-blob is not specified, we are reading the embedded
1227-
// snapshot, but we will skip it if --no-node-snapshot is specified.
1252+
return true;
1253+
}
1254+
1255+
if (per_process::cli_options->node_snapshot) {
1256+
// If --snapshot-blob is not specified or if the SEA contains not snapshot,
1257+
// we are reading the embedded snapshot, but we will skip it if
1258+
// --no-node-snapshot is specified.
12281259
const node::SnapshotData* read_data =
12291260
SnapshotBuilder::GetEmbeddedSnapshotData();
1230-
if (read_data != nullptr && read_data->Check()) {
1261+
if (read_data != nullptr) {
1262+
if (!read_data->Check()) {
1263+
return false;
1264+
}
12311265
// If we fail to read the embedded snapshot, treat it as if Node.js
12321266
// was built without one.
12331267
*snapshot_data_ptr = read_data;
12341268
}
12351269
}
12361270

1237-
NodeMainInstance main_instance(*snapshot_data_ptr,
1238-
uv_default_loop(),
1239-
per_process::v8_platform.Platform(),
1240-
result->args(),
1241-
result->exec_args());
1242-
exit_code = main_instance.Run();
1243-
return exit_code;
1271+
return true;
12441272
}
12451273

12461274
static ExitCode StartInternal(int argc, char** argv) {
@@ -1275,7 +1303,8 @@ static ExitCode StartInternal(int argc, char** argv) {
12751303

12761304
std::string sea_config = per_process::cli_options->experimental_sea_config;
12771305
if (!sea_config.empty()) {
1278-
return sea::BuildSingleExecutableBlob(sea_config);
1306+
return sea::BuildSingleExecutableBlob(
1307+
sea_config, result->args(), result->exec_args());
12791308
}
12801309

12811310
// --build-snapshot indicates that we are in snapshot building mode.
@@ -1290,7 +1319,15 @@ static ExitCode StartInternal(int argc, char** argv) {
12901319
}
12911320

12921321
// Without --build-snapshot, we are in snapshot loading mode.
1293-
return LoadSnapshotDataAndRun(&snapshot_data, result.get());
1322+
if (!LoadSnapshotData(&snapshot_data)) {
1323+
return ExitCode::kStartupSnapshotFailure;
1324+
}
1325+
NodeMainInstance main_instance(snapshot_data,
1326+
uv_default_loop(),
1327+
per_process::v8_platform.Platform(),
1328+
result->args(),
1329+
result->exec_args());
1330+
return main_instance.Run();
12941331
}
12951332

12961333
int Start(int argc, char** argv) {

src/node_main_instance.cc

+7-3
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,16 @@ void NodeMainInstance::Run(ExitCode* exit_code, Environment* env) {
9292
bool runs_sea_code = false;
9393
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
9494
if (sea::IsSingleExecutable()) {
95-
runs_sea_code = true;
9695
sea::SeaResource sea = sea::FindSingleExecutableResource();
97-
std::string_view code = sea.code;
98-
LoadEnvironment(env, code);
96+
if (!sea.use_snapshot()) {
97+
runs_sea_code = true;
98+
std::string_view code = sea.main_code_or_snapshot;
99+
LoadEnvironment(env, code);
100+
}
99101
}
100102
#endif
103+
// Either there is already a snapshot main function from SEA, or it's not
104+
// a SEA at all.
101105
if (!runs_sea_code) {
102106
LoadEnvironment(env, StartExecutionCallback{});
103107
}

src/node_sea.cc

+70-9
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
#include "json_parser.h"
77
#include "node_external_reference.h"
88
#include "node_internals.h"
9+
#include "node_snapshot_builder.h"
910
#include "node_union_bytes.h"
11+
#include "node_v8_platform-inl.h"
1012

1113
// The POSTJECT_SENTINEL_FUSE macro is a string of random characters selected by
1214
// the Node.js project that is present only once in the entire binary. It is
@@ -64,7 +66,7 @@ class SeaSerializer : public BlobSerializer<SeaSerializer> {
6466

6567
template <>
6668
size_t SeaSerializer::Write(const SeaResource& sea) {
67-
sink.reserve(SeaResource::kHeaderSize + sea.code.size());
69+
sink.reserve(SeaResource::kHeaderSize + sea.main_code_or_snapshot.size());
6870

6971
Debug("Write SEA magic %x\n", kMagic);
7072
size_t written_total = WriteArithmetic<uint32_t>(kMagic);
@@ -75,9 +77,12 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
7577
DCHECK_EQ(written_total, SeaResource::kHeaderSize);
7678

7779
Debug("Write SEA resource code %p, size=%zu\n",
78-
sea.code.data(),
79-
sea.code.size());
80-
written_total += WriteStringView(sea.code, StringLogMode::kAddressAndContent);
80+
sea.main_code_or_snapshot.data(),
81+
sea.main_code_or_snapshot.size());
82+
written_total +=
83+
WriteStringView(sea.main_code_or_snapshot,
84+
sea.use_snapshot() ? StringLogMode::kAddressOnly
85+
: StringLogMode::kAddressAndContent);
8186
return written_total;
8287
}
8388

@@ -103,7 +108,10 @@ SeaResource SeaDeserializer::Read() {
103108
Debug("Read SEA flags %x\n", static_cast<uint32_t>(flags));
104109
CHECK_EQ(read_total, SeaResource::kHeaderSize);
105110

106-
std::string_view code = ReadStringView(StringLogMode::kAddressAndContent);
111+
std::string_view code =
112+
ReadStringView(static_cast<bool>(flags & SeaFlags::kuseSnapshot)
113+
? StringLogMode::kAddressOnly
114+
: StringLogMode::kAddressAndContent);
107115
Debug("Read SEA resource code %p, size=%zu\n", code.data(), code.size());
108116
return {flags, code};
109117
}
@@ -133,6 +141,10 @@ std::string_view FindSingleExecutableBlob() {
133141

134142
} // anonymous namespace
135143

144+
bool SeaResource::use_snapshot() const {
145+
return static_cast<bool>(flags & SeaFlags::kuseSnapshot);
146+
}
147+
136148
SeaResource FindSingleExecutableResource() {
137149
static const SeaResource sea_resource = []() -> SeaResource {
138150
std::string_view blob = FindSingleExecutableBlob();
@@ -235,10 +247,23 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
235247
result.flags |= SeaFlags::kDisableExperimentalSeaWarning;
236248
}
237249

250+
std::optional<bool> use_snapshot = parser.GetTopLevelBoolField("useSnapshot");
251+
if (!use_snapshot.has_value()) {
252+
FPrintF(
253+
stderr, "\"useSnapshot\" field of %s is not a Boolean\n", config_path);
254+
return std::nullopt;
255+
}
256+
if (use_snapshot.value()) {
257+
result.flags |= SeaFlags::kuseSnapshot;
258+
}
259+
238260
return result;
239261
}
240262

241-
ExitCode GenerateSingleExecutableBlob(const SeaConfig& config) {
263+
ExitCode GenerateSingleExecutableBlob(
264+
const SeaConfig& config,
265+
const std::vector<std::string> args,
266+
const std::vector<std::string> exec_args) {
242267
std::string main_script;
243268
// TODO(joyeecheung): unify the file utils.
244269
int r = ReadFileSync(&main_script, config.main_path.c_str());
@@ -248,7 +273,40 @@ ExitCode GenerateSingleExecutableBlob(const SeaConfig& config) {
248273
return ExitCode::kGenericUserError;
249274
}
250275

251-
SeaResource sea{config.flags, main_script};
276+
std::vector<char> snapshot_blob;
277+
bool builds_snapshot_from_main =
278+
static_cast<bool>(config.flags & SeaFlags::kuseSnapshot);
279+
if (builds_snapshot_from_main) {
280+
SnapshotData snapshot;
281+
std::vector<std::string> patched_args = {args[0], GetAnonymousMainPath()};
282+
ExitCode exit_code = SnapshotBuilder::Generate(
283+
&snapshot, patched_args, exec_args, main_script);
284+
if (exit_code != ExitCode::kNoFailure) {
285+
return exit_code;
286+
}
287+
auto& persistents = snapshot.env_info.principal_realm.persistent_values;
288+
auto it = std::find_if(
289+
persistents.begin(), persistents.end(), [](const PropInfo& prop) {
290+
return prop.name == "snapshot_deserialize_main";
291+
});
292+
if (it == persistents.end()) {
293+
FPrintF(
294+
stderr,
295+
"%s does not invoke "
296+
"v8.startupSnapshot.setDeserializeMainFunction(), which is required "
297+
"for snapshot scripts used to build single executable applications."
298+
"\n",
299+
config.main_path);
300+
return ExitCode::kGenericUserError;
301+
}
302+
snapshot.ToBlob(&snapshot_blob);
303+
}
304+
305+
SeaResource sea{
306+
config.flags,
307+
builds_snapshot_from_main
308+
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
309+
: std::string_view{main_script.data(), main_script.size()}};
252310

253311
SeaSerializer serializer;
254312
serializer.Write(sea);
@@ -269,11 +327,14 @@ ExitCode GenerateSingleExecutableBlob(const SeaConfig& config) {
269327

270328
} // anonymous namespace
271329

272-
ExitCode BuildSingleExecutableBlob(const std::string& config_path) {
330+
ExitCode BuildSingleExecutableBlob(const std::string& config_path,
331+
const std::vector<std::string> args,
332+
const std::vector<std::string> exec_args) {
273333
std::optional<SeaConfig> config_opt =
274334
ParseSingleExecutableConfig(config_path);
275335
if (config_opt.has_value()) {
276-
ExitCode code = GenerateSingleExecutableBlob(config_opt.value());
336+
ExitCode code =
337+
GenerateSingleExecutableBlob(config_opt.value(), args, exec_args);
277338
return code;
278339
}
279340

src/node_sea.h

+7-2
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,24 @@ const uint32_t kMagic = 0x143da20;
2121
enum class SeaFlags : uint32_t {
2222
kDefault = 0,
2323
kDisableExperimentalSeaWarning = 1 << 0,
24+
kuseSnapshot = 1 << 1,
2425
};
2526

2627
struct SeaResource {
2728
SeaFlags flags = SeaFlags::kDefault;
28-
std::string_view code;
29+
std::string_view main_code_or_snapshot;
2930

31+
bool use_snapshot() const;
3032
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
3133
};
3234

3335
bool IsSingleExecutable();
3436
SeaResource FindSingleExecutableResource();
3537
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv);
36-
node::ExitCode BuildSingleExecutableBlob(const std::string& config_path);
38+
node::ExitCode BuildSingleExecutableBlob(
39+
const std::string& config_path,
40+
const std::vector<std::string> args,
41+
const std::vector<std::string> exec_args);
3742
} // namespace sea
3843
} // namespace node
3944

0 commit comments

Comments
 (0)