@wasm-oj/server 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ use crate::{
6
6
  },
7
7
  types::DeterminismConfig,
8
8
  };
9
+ use std::cell::Cell;
9
10
  use std::sync::{Arc, Mutex};
10
11
  use wasmer::{
11
12
  AsStoreMut, Extern, Function, FunctionEnv, FunctionEnvMut, Imports, Memory, Memory32, Memory64,
@@ -239,6 +240,23 @@ struct PollEnv {
239
240
  clock: VirtualClock,
240
241
  }
241
242
 
243
+ thread_local! {
244
+ static STDIN_READINESS_DEFERRALS: Cell<u64> = const { Cell::new(0) };
245
+ }
246
+
247
+ /// Whether an empty stdin readiness check may return `Pending` instead of
248
+ /// blocking. A clock probe never blocks. A poll without a clock grants one
249
+ /// deferral per read subscription, so WASIX reports any other ready
250
+ /// subscription first and blocks on stdin only when it would wait anyway.
251
+ #[cfg(target_arch = "wasm32")]
252
+ pub(crate) fn defer_stdin_readiness() -> bool {
253
+ STDIN_READINESS_DEFERRALS.with(|deferrals| {
254
+ let remaining = deferrals.get();
255
+ deferrals.set(remaining.saturating_sub(1));
256
+ remaining > 0
257
+ })
258
+ }
259
+
242
260
  #[derive(Clone, Copy)]
243
261
  struct ClockSubscription {
244
262
  subscription: Subscription,
@@ -251,6 +269,7 @@ fn call_original_poll<M: MemorySize>(
251
269
  events: WasmPtr<Event, M>,
252
270
  subscription_count: M::Offset,
253
271
  event_count: WasmPtr<M::Offset, M>,
272
+ stdin_deferrals: u64,
254
273
  ) -> Result<i32, RuntimeError> {
255
274
  let Some(original) = env.data().original.clone() else {
256
275
  return Ok(Errno::Notsup as i32);
@@ -262,6 +281,7 @@ fn call_original_poll<M: MemorySize>(
262
281
  Value::I32(offset as i32)
263
282
  }
264
283
  };
284
+ STDIN_READINESS_DEFERRALS.with(|deferrals| deferrals.set(stdin_deferrals));
265
285
  let results = original.call(
266
286
  env,
267
287
  &[
@@ -270,7 +290,9 @@ fn call_original_poll<M: MemorySize>(
270
290
  pointer(subscription_count.into()),
271
291
  pointer(event_count.offset().into()),
272
292
  ],
273
- )?;
293
+ );
294
+ STDIN_READINESS_DEFERRALS.with(|deferrals| deferrals.set(0));
295
+ let results = results?;
274
296
  match results.as_ref() {
275
297
  [Value::I32(errno)] => Ok(*errno),
276
298
  _ => Err(RuntimeError::new(
@@ -302,6 +324,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
302
324
  let mut originals = Vec::with_capacity(input.len() as usize);
303
325
  let mut clock_subscriptions = Vec::new();
304
326
  let mut has_non_clock = false;
327
+ let mut read_subscriptions = 0;
305
328
  for index in 0..input.len() {
306
329
  let subscription = input
307
330
  .index(index)
@@ -320,6 +343,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
320
343
  });
321
344
  } else {
322
345
  has_non_clock = true;
346
+ read_subscriptions += u64::from(subscription.type_ == Eventtype::FdRead);
323
347
  }
324
348
  }
325
349
 
@@ -330,6 +354,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
330
354
  events,
331
355
  subscription_count,
332
356
  event_count,
357
+ read_subscriptions,
333
358
  );
334
359
  }
335
360
 
@@ -355,6 +380,7 @@ fn deterministic_poll_oneoff<M: MemorySize>(
355
380
  events,
356
381
  subscription_count,
357
382
  event_count,
383
+ u64::MAX,
358
384
  );
359
385
  let restore_view = memory.view(&env);
360
386
  let restore_input = subscriptions
@@ -3,10 +3,7 @@ use crate::deterministic::{VirtualClock, attach_interactive_deterministic_import
3
3
  use crate::filesystem::{
4
4
  RuntimeProjectFilesystem, is_normalized_guest_path, runtime_project_files,
5
5
  };
6
- use crate::meter::{
7
- CONTESTANT_METERING_MODULE, HOST_GAS_FUNCTION, INTERACTOR_METERING_MODULE,
8
- instrument_wasm_with_host_meter,
9
- };
6
+ use crate::meter::{CostPoints, MeterState, instrument_wasm, meter_state, remaining_points};
10
7
  use crate::module_imports::attach_declared_memory_imports;
11
8
  use crate::module_policy::{
12
9
  DEFERRED_START_EXPORT, defer_start_section, enforce_memory_limit,
@@ -17,6 +14,7 @@ use crate::{
17
14
  ExecutionTermination, InteractiveMetrics, InteractiveProcessResult, InteractiveProgram,
18
15
  InteractiveRequest, InteractiveResult, RunError,
19
16
  };
17
+ use futures::channel::oneshot;
20
18
  use std::collections::{BTreeMap, HashMap};
21
19
  use std::io;
22
20
  use std::pin::Pin;
@@ -24,75 +22,17 @@ use std::sync::{Arc, Mutex};
24
22
  use std::task::{Context, Poll};
25
23
  use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
26
24
  use virtual_fs::{FsError, Pipe, PipeRx, PipeTx, VirtualFile};
27
- use wasmer::{
28
- AsStoreMut, Engine, Function, FunctionEnv, FunctionEnvMut, Imports, Memory, RuntimeError,
29
- };
25
+ use wasmer::{AsStoreMut, Engine, Imports, Memory};
30
26
  use wasmer_types::StoreId;
31
- use wasmer_wasix::bin_factory::spawn_exec_wasm;
27
+ use wasmer_wasix::bin_factory::{run_exec, spawn_load_module};
28
+ use wasmer_wasix::os::task::TaskJoinHandle;
32
29
  use wasmer_wasix::runtime::module_cache::{self, HashedModuleData, ModuleCache};
30
+ use wasmer_wasix::runtime::task_manager::TaskWasm;
33
31
  use wasmer_wasix::{
34
32
  PluggableRuntime, Runtime, WasiEnv, WasiFunctionEnv, WasiRuntimeError, WasiVersion,
35
33
  generate_import_object_from_env,
36
34
  };
37
35
 
38
- #[derive(Debug)]
39
- struct GasBudget {
40
- initial: u64,
41
- state: Mutex<GasState>,
42
- }
43
-
44
- #[derive(Debug)]
45
- struct GasState {
46
- remaining: u64,
47
- exhausted: bool,
48
- }
49
-
50
- impl GasBudget {
51
- fn new(initial: u64) -> Self {
52
- Self {
53
- initial,
54
- state: Mutex::new(GasState {
55
- remaining: initial,
56
- exhausted: false,
57
- }),
58
- }
59
- }
60
-
61
- fn charge(&self, amount: u64) -> Result<(), RuntimeError> {
62
- let mut state = self
63
- .state
64
- .lock()
65
- .map_err(|error| RuntimeError::new(error.to_string()))?;
66
- if amount > state.remaining {
67
- state.remaining = 0;
68
- state.exhausted = true;
69
- return Err(RuntimeError::new("WASM-OJ instruction budget exhausted"));
70
- }
71
- state.remaining -= amount;
72
- Ok(())
73
- }
74
-
75
- fn metrics(&self) -> Result<(u64, bool), RunError> {
76
- let state = self
77
- .state
78
- .lock()
79
- .map_err(|error| RunError::Runtime(error.to_string()))?;
80
- Ok((
81
- self.initial.saturating_sub(state.remaining),
82
- state.exhausted,
83
- ))
84
- }
85
- }
86
-
87
- #[derive(Clone)]
88
- struct GasEnv(Arc<GasBudget>);
89
-
90
- fn charge_gas(env: FunctionEnvMut<GasEnv>, amount: i64) -> Result<(), RuntimeError> {
91
- let amount = u64::try_from(amount)
92
- .map_err(|_| RuntimeError::new("WASM-OJ received a negative instruction charge"))?;
93
- env.data().0.charge(amount)
94
- }
95
-
96
36
  #[derive(Debug)]
97
37
  struct InteractiveInput {
98
38
  pipe: PipeRx,
@@ -263,7 +203,8 @@ impl VirtualFile for InteractiveOutput {
263
203
  struct PreparedProgram {
264
204
  wasm: Vec<u8>,
265
205
  operations: BTreeMap<String, u64>,
266
- gas: Arc<GasBudget>,
206
+ budget: u64,
207
+ meter: Arc<Mutex<Option<MeterState>>>,
267
208
  protocol: OutputCapture,
268
209
  stderr: OutputCapture,
269
210
  filesystem: RuntimeProjectFilesystem,
@@ -272,16 +213,8 @@ struct PreparedProgram {
272
213
 
273
214
  pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult, RunError> {
274
215
  validate_request(&request)?;
275
- let contestant = prepare_program(
276
- &request.contestant,
277
- CONTESTANT_METERING_MODULE,
278
- &request.determinism,
279
- )?;
280
- let interactor = prepare_program(
281
- &request.interactor,
282
- INTERACTOR_METERING_MODULE,
283
- &request.determinism,
284
- )?;
216
+ let contestant = prepare_program(&request.contestant, &request.determinism)?;
217
+ let interactor = prepare_program(&request.interactor, &request.determinism)?;
285
218
  let maximum_memory = request
286
219
  .contestant
287
220
  .resources
@@ -293,7 +226,7 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
293
226
  let contestant_runtime = interactive_runtime(
294
227
  engine.clone(),
295
228
  &request.determinism,
296
- contestant.gas.clone(),
229
+ contestant.meter.clone(),
297
230
  contestant.clock.clone(),
298
231
  request.contestant.startup_entropy_bytes,
299
232
  module_cache.clone(),
@@ -303,7 +236,7 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
303
236
  let interactor_runtime = interactive_runtime(
304
237
  engine,
305
238
  &request.determinism,
306
- interactor.gas.clone(),
239
+ interactor.meter.clone(),
307
240
  interactor.clock.clone(),
308
241
  request.interactor.startup_entropy_bytes,
309
242
  module_cache,
@@ -334,36 +267,38 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
334
267
  *interactor_wasi
335
268
  .lock()
336
269
  .map_err(|error| RunError::Runtime(error.to_string()))? = Some(interactor_env.clone());
337
- let mut contestant_handle = spawn_exec_wasm(
338
- HashedModuleData::new(contestant.wasm.clone()),
270
+ let (mut contestant_handle, contestant_points) = spawn_metered(
339
271
  "contestant",
272
+ &contestant,
340
273
  contestant_env,
341
274
  &contestant_runtime,
342
275
  )
343
- .await
344
- .map_err(|error| RunError::Compile(format!("failed to start contestant: {error}")))?;
345
- let mut interactor_handle = spawn_exec_wasm(
346
- HashedModuleData::new(interactor.wasm.clone()),
276
+ .await?;
277
+ let (mut interactor_handle, interactor_points) = spawn_metered(
347
278
  "interactor",
279
+ &interactor,
348
280
  interactor_env,
349
281
  &interactor_runtime,
350
282
  )
351
- .await
352
- .map_err(|error| RunError::Compile(format!("failed to start interactor: {error}")))?;
353
- let (contestant_status, interactor_status) = futures::join!(
283
+ .await?;
284
+ let (contestant_status, interactor_status, contestant_points, interactor_points) = futures::join!(
354
285
  contestant_handle.wait_finished(),
355
286
  interactor_handle.wait_finished(),
287
+ contestant_points,
288
+ interactor_points,
356
289
  );
357
290
 
358
291
  let contestant_to_interactor = contestant.protocol.bytes();
359
292
  let interactor_to_contestant = interactor.protocol.bytes();
360
293
  let contestant_result = process_result(
361
294
  contestant_status,
295
+ contestant_points,
362
296
  contestant,
363
297
  contestant_to_interactor.len(),
364
298
  )?;
365
299
  let interactor_result = process_result(
366
300
  interactor_status,
301
+ interactor_points,
367
302
  interactor,
368
303
  interactor_to_contestant.len(),
369
304
  )?;
@@ -376,15 +311,56 @@ pub async fn interact(request: InteractiveRequest) -> Result<InteractiveResult,
376
311
  })
377
312
  }
378
313
 
314
+ /// `spawn_exec_wasm` without its CLOEXEC sweep, which a fresh environment does not need, plus a
315
+ /// recycle hook that reads the meter from the store after the process exits. WASIX reports the
316
+ /// exit before it recycles the store, so callers await both.
317
+ async fn spawn_metered(
318
+ name: &str,
319
+ program: &PreparedProgram,
320
+ env: WasiEnv,
321
+ runtime: &Arc<dyn Runtime + Send + Sync>,
322
+ ) -> Result<
323
+ (
324
+ TaskJoinHandle,
325
+ oneshot::Receiver<Result<CostPoints, String>>,
326
+ ),
327
+ RunError,
328
+ > {
329
+ let module = spawn_load_module(name, HashedModuleData::new(program.wasm.clone()), runtime)
330
+ .await
331
+ .map_err(|error| RunError::Compile(format!("failed to start {name}: {error}")))?;
332
+ let finished = env.thread.join_handle();
333
+ let meter = program.meter.clone();
334
+ let (points, received) = oneshot::channel();
335
+ let task = TaskWasm::new(Box::new(run_exec), env, module, true, true).with_recycle(Box::new(
336
+ move |mut exited| {
337
+ let read = meter
338
+ .lock()
339
+ .map_err(|error| error.to_string())
340
+ .and_then(|meter| {
341
+ meter
342
+ .clone()
343
+ .ok_or_else(|| "meter is unavailable".to_string())
344
+ })
345
+ .and_then(|meter| remaining_points(&mut exited.store, &meter));
346
+ let _ = points.send(read);
347
+ },
348
+ ));
349
+ runtime
350
+ .task_manager()
351
+ .task_wasm(task)
352
+ .map_err(|error| RunError::Compile(format!("failed to start {name}: {error}")))?;
353
+ Ok((finished, received))
354
+ }
355
+
379
356
  fn prepare_program(
380
357
  program: &InteractiveProgram,
381
- metering_module: &'static str,
382
358
  determinism: &crate::DeterminismConfig,
383
359
  ) -> Result<PreparedProgram, RunError> {
384
360
  let limited = enforce_memory_limit(&program.wasm, program.resources.memory_limit_bytes)
385
361
  .map_err(RunError::Compile)?;
386
- let metered =
387
- instrument_wasm_with_host_meter(&limited, metering_module).map_err(RunError::Compile)?;
362
+ let metered = instrument_wasm(&limited, program.resources.instruction_budget)
363
+ .map_err(RunError::Compile)?;
388
364
  let executable = defer_start_section(&metered.wasm).map_err(RunError::Compile)?;
389
365
  let wasm =
390
366
  rewrite_interactive_deterministic_imports(&executable.wasm).map_err(RunError::Compile)?;
@@ -397,7 +373,8 @@ fn prepare_program(
397
373
  Ok(PreparedProgram {
398
374
  wasm,
399
375
  operations: metered.operations,
400
- gas: Arc::new(GasBudget::new(program.resources.instruction_budget)),
376
+ budget: program.resources.instruction_budget,
377
+ meter: Arc::new(Mutex::new(None)),
401
378
  protocol,
402
379
  stderr,
403
380
  filesystem,
@@ -443,12 +420,20 @@ fn build_environment(
443
420
 
444
421
  fn process_result(
445
422
  status: Result<wasmer_wasix::wasmer_wasix_types::wasi::ExitCode, Arc<WasiRuntimeError>>,
423
+ points: Result<Result<CostPoints, String>, oneshot::Canceled>,
446
424
  prepared: PreparedProgram,
447
425
  protocol_bytes: usize,
448
426
  ) -> Result<InteractiveProcessResult, RunError> {
449
427
  let stderr = prepared.stderr.bytes();
450
428
  let output_exceeded = prepared.protocol.exceeded() || prepared.stderr.exceeded();
451
- let (cost, exhausted) = prepared.gas.metrics()?;
429
+ let points = points
430
+ .map_err(|_| RunError::Runtime("interactive process exited without its meter".to_string()))?
431
+ .map_err(RunError::Runtime)?;
432
+ let exhausted = points == CostPoints::Exhausted;
433
+ let cost = match points {
434
+ CostPoints::Remaining(points) => prepared.budget.saturating_sub(points),
435
+ CostPoints::Exhausted => prepared.budget,
436
+ };
452
437
  let logical_time_exceeded = prepared.clock.limit_exceeded()?;
453
438
  let (code, termination) = if prepared.filesystem.quota_exceeded() {
454
439
  (137, ExecutionTermination::FilesystemLimit)
@@ -516,7 +501,7 @@ fn validate_request(request: &InteractiveRequest) -> Result<(), RunError> {
516
501
  fn interactive_runtime(
517
502
  engine: Engine,
518
503
  determinism: &crate::DeterminismConfig,
519
- gas: Arc<GasBudget>,
504
+ meter: Arc<Mutex<Option<MeterState>>>,
520
505
  clock: VirtualClock,
521
506
  startup_entropy_bytes: u64,
522
507
  module_cache: Arc<dyn ModuleCache + Send + Sync>,
@@ -578,22 +563,6 @@ fn interactive_runtime(
578
563
  clock.clone(),
579
564
  startup_entropy_bytes,
580
565
  );
581
- let metering_module = module
582
- .imports()
583
- .find_map(|import| match import.module() {
584
- CONTESTANT_METERING_MODULE => Some(CONTESTANT_METERING_MODULE),
585
- INTERACTOR_METERING_MODULE => Some(INTERACTOR_METERING_MODULE),
586
- _ => None,
587
- })
588
- .ok_or_else(|| {
589
- io::Error::other("interactive module has no WASM-OJ metering identity")
590
- })?;
591
- let gas_env = FunctionEnv::new(&mut *store, GasEnv(gas.clone()));
592
- imports.define(
593
- metering_module,
594
- HOST_GAS_FUNCTION,
595
- Function::new_typed_with_env(&mut *store, &gas_env, charge_gas),
596
- );
597
566
  attach_capability_denials(store, module, &mut imports).map_err(io::Error::other)?;
598
567
  attach_declared_memory_imports(store, module, &mut imports).map_err(io::Error::other)?;
599
568
  Ok(imports)
@@ -616,6 +585,10 @@ fn interactive_runtime(
616
585
  .memory
617
586
  .lock()
618
587
  .map_err(|error| io::Error::other(error.to_string()))? = Some(memory);
588
+ *meter
589
+ .lock()
590
+ .map_err(|error| io::Error::other(error.to_string()))? =
591
+ Some(meter_state(instance).map_err(io::Error::other)?);
619
592
  pending
620
593
  .wasi
621
594
  .initialize(&mut *store, instance.clone())
@@ -673,7 +646,7 @@ mod tests {
673
646
  use super::interact;
674
647
  use crate::{
675
648
  DeterminismConfig, ExecutionTermination, InteractiveProgram, InteractiveRequest,
676
- ResourcePolicy,
649
+ ResourcePolicy, RunRequest,
677
650
  };
678
651
  use std::collections::BTreeMap;
679
652
 
@@ -998,6 +971,155 @@ mod tests {
998
971
  assert_eq!(result.interactor.metrics.logical_time_ns, 0);
999
972
  }
1000
973
 
974
+ #[test]
975
+ fn interactive_empty_stdin_poll_times_out_on_the_process_clock() {
976
+ let poller = wat::parse_str(
977
+ r#"(module
978
+ (import "wasi_snapshot_preview1" "poll_oneoff"
979
+ (func $poll (param i32 i32 i32 i32) (result i32)))
980
+ (import "wasi_snapshot_preview1" "fd_write"
981
+ (func $fd_write (param i32 i32 i32 i32) (result i32)))
982
+ (memory (export "memory") 1)
983
+ (func (export "_start")
984
+ i32.const 0 i64.const 1 i64.store
985
+ i32.const 8 i32.const 1 i32.store8
986
+ i32.const 16 i32.const 0 i32.store
987
+ i32.const 48 i64.const 2 i64.store
988
+ i32.const 56 i32.const 0 i32.store8
989
+ i32.const 64 i32.const 1 i32.store
990
+ i32.const 72 i64.const 5000000000 i64.store
991
+ i32.const 80 i64.const 1 i64.store
992
+ i32.const 88 i32.const 0 i32.store16
993
+ i32.const 0 i32.const 128 i32.const 2 i32.const 240 call $poll drop
994
+ i32.const 200 i32.const 138 i32.store
995
+ i32.const 204 i32.const 1 i32.store
996
+ i32.const 208 i32.const 240 i32.store
997
+ i32.const 212 i32.const 1 i32.store
998
+ i32.const 1 i32.const 200 i32.const 2 i32.const 216 call $fd_write drop))"#,
999
+ )
1000
+ .unwrap();
1001
+ let listener = wat::parse_str(
1002
+ r#"(module
1003
+ (import "wasi_snapshot_preview1" "fd_read"
1004
+ (func $fd_read (param i32 i32 i32 i32) (result i32)))
1005
+ (memory (export "memory") 1)
1006
+ (func (export "_start")
1007
+ (i32.store (i32.const 0) (i32.const 64))
1008
+ (i32.store (i32.const 4) (i32.const 8))
1009
+ (drop (call $fd_read (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)))))"#,
1010
+ )
1011
+ .unwrap();
1012
+ let runtime = tokio::runtime::Builder::new_current_thread()
1013
+ .enable_all()
1014
+ .build()
1015
+ .unwrap();
1016
+ let result = runtime
1017
+ .block_on(interact(InteractiveRequest {
1018
+ contestant: program(poller),
1019
+ interactor: program(listener),
1020
+ determinism: DeterminismConfig {
1021
+ random_seed: 7,
1022
+ realtime_epoch_ms: 946_684_800_000,
1023
+ clock_step_ns: 1_000_000,
1024
+ },
1025
+ }))
1026
+ .unwrap();
1027
+
1028
+ assert_eq!(result.contestant_to_interactor, [0, 1]);
1029
+ assert_eq!(result.contestant.termination, ExecutionTermination::Exited);
1030
+ assert_eq!(result.contestant.metrics.logical_time_ns, 5_000_000_000);
1031
+ assert_eq!(result.interactor.termination, ExecutionTermination::Exited);
1032
+ }
1033
+
1034
+ #[test]
1035
+ fn interactive_contestant_is_metered_like_a_standalone_run() {
1036
+ let looping = |ending: &str| {
1037
+ wat::parse_str(format!(
1038
+ r#"(module
1039
+ (import "wasi_snapshot_preview1" "proc_exit" (func $exit (param i32)))
1040
+ (memory (export "memory") 1)
1041
+ (func (export "_start")
1042
+ (local $remaining i32)
1043
+ i32.const 1000 local.set $remaining
1044
+ (loop $again
1045
+ local.get $remaining i32.const 1 i32.sub local.tee $remaining
1046
+ br_if $again)
1047
+ {ending}))"#
1048
+ ))
1049
+ .unwrap()
1050
+ };
1051
+ let determinism = DeterminismConfig {
1052
+ random_seed: 7,
1053
+ realtime_epoch_ms: 946_684_800_000,
1054
+ clock_step_ns: 1_000_000,
1055
+ };
1056
+ let execute = |wasm: &[u8], instruction_budget: u64| {
1057
+ let mut contestant = program(wasm.to_vec());
1058
+ contestant.resources.instruction_budget = instruction_budget;
1059
+ let standalone = crate::run(RunRequest {
1060
+ wasm: contestant.wasm.clone(),
1061
+ args: Vec::new(),
1062
+ env: BTreeMap::new(),
1063
+ stdin: Vec::new(),
1064
+ files: BTreeMap::new(),
1065
+ output_paths: Vec::new(),
1066
+ cwd: contestant.cwd.clone(),
1067
+ startup_entropy_bytes: 0,
1068
+ determinism: determinism.clone(),
1069
+ resources: contestant.resources.clone(),
1070
+ })
1071
+ .unwrap();
1072
+ let interactive = tokio::runtime::Builder::new_current_thread()
1073
+ .enable_all()
1074
+ .build()
1075
+ .unwrap()
1076
+ .block_on(interact(InteractiveRequest {
1077
+ interactor: contestant.clone(),
1078
+ contestant,
1079
+ determinism: determinism.clone(),
1080
+ }))
1081
+ .unwrap();
1082
+ assert_eq!(interactive.interactor.code, interactive.contestant.code);
1083
+ assert_eq!(
1084
+ interactive.interactor.termination,
1085
+ interactive.contestant.termination
1086
+ );
1087
+ assert_eq!(
1088
+ interactive.interactor.metrics.cost,
1089
+ interactive.contestant.metrics.cost
1090
+ );
1091
+ (standalone, interactive.contestant)
1092
+ };
1093
+
1094
+ for (ending, code) in [("", 0), ("i32.const 3 call $exit", 3)] {
1095
+ let wasm = looping(ending);
1096
+ let (standalone, interactive) = execute(&wasm, 1_000_000);
1097
+ assert_eq!(standalone.termination, ExecutionTermination::Exited);
1098
+ assert_eq!(standalone.code, code);
1099
+ assert_eq!(interactive.termination, ExecutionTermination::Exited);
1100
+ assert_eq!(interactive.code, code);
1101
+ assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
1102
+
1103
+ let budget = standalone.metrics.cost - 1;
1104
+ let (standalone, interactive) = execute(&wasm, budget);
1105
+ assert_eq!(
1106
+ standalone.termination,
1107
+ ExecutionTermination::InstructionLimit
1108
+ );
1109
+ assert_eq!(
1110
+ interactive.termination,
1111
+ ExecutionTermination::InstructionLimit
1112
+ );
1113
+ assert_eq!(interactive.code, 137);
1114
+ assert_eq!(interactive.metrics.cost, budget);
1115
+ assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
1116
+ }
1117
+
1118
+ let (standalone, interactive) = execute(&looping("unreachable"), 1_000_000);
1119
+ assert_eq!(standalone.termination, ExecutionTermination::Trap);
1120
+ assert_eq!(interactive.metrics.cost, standalone.metrics.cost);
1121
+ }
1122
+
1001
1123
  fn program(wasm: Vec<u8>) -> InteractiveProgram {
1002
1124
  InteractiveProgram {
1003
1125
  wasm,
@@ -7,18 +7,13 @@ use std::borrow::Cow;
7
7
  use std::collections::BTreeMap;
8
8
  use wasm_encoder::reencode::{Error as ReencodeError, Reencode};
9
9
  use wasm_encoder::{Encode, Section};
10
- #[cfg(not(target_arch = "wasm32"))]
11
- use wasmer::Global;
12
10
  #[cfg(target_arch = "wasm32")]
13
11
  use wasmer::js::AsJs;
14
- use wasmer::{AsStoreMut, Instance};
12
+ use wasmer::{AsStoreMut, Global, Instance};
15
13
 
16
14
  pub const METER_MODEL: &str = "weighted";
17
15
  const METERING_MODULE: &str = "wasm_oj_metering";
18
16
  const GAS_COUNTER_NAME: &str = "gas_counter";
19
- pub(crate) const CONTESTANT_METERING_MODULE: &str = "wasm_oj_contestant_metering";
20
- pub(crate) const INTERACTOR_METERING_MODULE: &str = "wasm_oj_interactor_metering";
21
- pub(crate) const HOST_GAS_FUNCTION: &str = "charge";
22
17
 
23
18
  #[derive(Debug)]
24
19
  pub struct InstrumentedModule {
@@ -53,10 +48,7 @@ pub enum CostPoints {
53
48
 
54
49
  #[derive(Clone, Debug)]
55
50
  pub struct MeterState {
56
- #[cfg(not(target_arch = "wasm32"))]
57
51
  gas_counter: Global,
58
- #[cfg(target_arch = "wasm32")]
59
- gas_counter: WebAssembly::Global,
60
52
  }
61
53
 
62
54
  pub fn instrument_wasm(wasm: &[u8], budget: u64) -> Result<InstrumentedModule, String> {
@@ -85,32 +77,6 @@ pub fn instrument_wasm(wasm: &[u8], budget: u64) -> Result<InstrumentedModule, S
85
77
  })
86
78
  }
87
79
 
88
- pub(crate) fn instrument_wasm_with_host_meter(
89
- wasm: &[u8],
90
- metering_module: &'static str,
91
- ) -> Result<InstrumentedModule, String> {
92
- let runtime_sections = runtime_custom_sections(wasm)?;
93
- let executable = canonicalize_custom_sections(wasm)?;
94
- let operations = inspect_weighted_opcodes(&executable)?;
95
- let mut module = ModuleInfo::new(&executable)
96
- .map_err(|error| format!("failed to parse module for weighted metering: {error}"))?;
97
- let backend = gas_metering::host_function::Injector::new(metering_module, HOST_GAS_FUNCTION);
98
- let mut metered = gas_metering::inject(&mut module, backend, &WeightedRules)
99
- .map_err(|error| format!("failed to inject weighted host metering: {error}"))?;
100
- for (name, data) in runtime_sections {
101
- let section = wasm_encoder::CustomSection {
102
- name: Cow::Owned(name),
103
- data: Cow::Owned(data),
104
- };
105
- metered.push(section.id());
106
- section.encode(&mut metered);
107
- }
108
- Ok(InstrumentedModule {
109
- wasm: metered,
110
- operations,
111
- })
112
- }
113
-
114
80
  #[derive(Debug)]
115
81
  struct MeterInitializationError(String);
116
82
 
@@ -271,26 +237,13 @@ fn inspect_weighted_opcodes(wasm: &[u8]) -> Result<BTreeMap<String, u64>, String
271
237
  Ok(operations)
272
238
  }
273
239
 
274
- pub fn meter_state(store: &mut impl AsStoreMut, instance: &Instance) -> Result<MeterState, String> {
275
- let gas_counter = instance
240
+ pub fn meter_state(instance: &Instance) -> Result<MeterState, String> {
241
+ instance
276
242
  .exports
277
243
  .get_global(GAS_COUNTER_NAME)
278
- .map_err(|error| format!("instrumented module does not export its meter: {error}"))?
279
- .clone();
280
-
281
- #[cfg(not(target_arch = "wasm32"))]
282
- {
283
- let _ = store;
284
- Ok(MeterState { gas_counter })
285
- }
286
-
287
- #[cfg(target_arch = "wasm32")]
288
- {
289
- let js_global: WebAssembly::Global = gas_counter.as_jsvalue(store).into();
290
- Ok(MeterState {
291
- gas_counter: js_global,
292
- })
293
- }
244
+ .cloned()
245
+ .map(|gas_counter| MeterState { gas_counter })
246
+ .map_err(|error| format!("instrumented module does not export its meter: {error}"))
294
247
  }
295
248
 
296
249
  pub fn remaining_points(
@@ -306,8 +259,8 @@ pub fn remaining_points(
306
259
 
307
260
  #[cfg(target_arch = "wasm32")]
308
261
  let value = {
309
- let _ = store;
310
- i64::try_from(BigInt::from(meter.gas_counter.value()))
262
+ let gas_counter: WebAssembly::Global = meter.gas_counter.as_jsvalue(store).into();
263
+ i64::try_from(BigInt::from(gas_counter.value()))
311
264
  .map_err(|_| "metering global is outside the signed 64-bit range".to_string())?
312
265
  };
313
266
 
@@ -15,6 +15,8 @@ mod native;
15
15
  #[cfg(target_arch = "wasm32")]
16
16
  mod web;
17
17
  #[cfg(target_arch = "wasm32")]
18
+ pub(crate) mod web_interactive;
19
+ #[cfg(target_arch = "wasm32")]
18
20
  pub(crate) mod web_runtime;
19
21
 
20
22
  pub fn run(request: RunRequest) -> Result<RunResult, RunError> {