github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/imports/emscripten/emscripten_test.go (about)

     1  package emscripten
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	_ "embed"
     7  	"testing"
     8  
     9  	wazero "github.com/wasilibs/wazerox"
    10  	"github.com/wasilibs/wazerox/api"
    11  	"github.com/wasilibs/wazerox/experimental"
    12  	"github.com/wasilibs/wazerox/experimental/logging"
    13  	"github.com/wasilibs/wazerox/imports/wasi_snapshot_preview1"
    14  	internal "github.com/wasilibs/wazerox/internal/emscripten"
    15  	"github.com/wasilibs/wazerox/internal/testing/binaryencoding"
    16  	"github.com/wasilibs/wazerox/internal/testing/require"
    17  	"github.com/wasilibs/wazerox/internal/wasm"
    18  )
    19  
    20  const (
    21  	i64 = wasm.ValueTypeI64
    22  	f32 = wasm.ValueTypeF32
    23  	f64 = wasm.ValueTypeF64
    24  )
    25  
    26  // growWasm was compiled from testdata/grow.cc
    27  //
    28  //go:embed testdata/grow.wasm
    29  var growWasm []byte
    30  
    31  // invokeWasm was generated by the following:
    32  //
    33  //	cd testdata; wat2wasm --debug-names invoke.wat
    34  //
    35  //go:embed testdata/invoke.wasm
    36  var invokeWasm []byte
    37  
    38  // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
    39  var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
    40  
    41  // TestGrow is an integration test until we have an Emscripten example.
    42  func TestGrow(t *testing.T) {
    43  	var log bytes.Buffer
    44  
    45  	// Set context to one that has an experimental listener
    46  	ctx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{},
    47  		logging.NewHostLoggingListenerFactory(&log, logging.LogScopeMemory))
    48  
    49  	r := wazero.NewRuntime(ctx)
    50  	defer r.Close(ctx)
    51  
    52  	wasi_snapshot_preview1.MustInstantiate(ctx, r)
    53  
    54  	_, err := Instantiate(ctx, r)
    55  	require.NoError(t, err)
    56  
    57  	// Emscripten exits main with zero by default, which coerces to nul.
    58  	_, err = r.Instantiate(ctx, growWasm)
    59  	require.Nil(t, err)
    60  
    61  	// We expect the memory no-op memory growth hook to be invoked as wasm.
    62  	require.Contains(t, log.String(), "==> env.emscripten_notify_memory_growth(memory_index=0)")
    63  }
    64  
    65  func TestNewFunctionExporterForModule(t *testing.T) {
    66  	tests := []struct {
    67  		name     string
    68  		input    *wasm.Module
    69  		expected emscriptenFns
    70  	}{
    71  		{
    72  			name:     "empty",
    73  			input:    &wasm.Module{},
    74  			expected: emscriptenFns{},
    75  		},
    76  		{
    77  			name: internal.FunctionNotifyMemoryGrowth,
    78  			input: &wasm.Module{
    79  				TypeSection: []wasm.FunctionType{
    80  					{Params: []wasm.ValueType{i32}},
    81  				},
    82  				ImportSection: []wasm.Import{
    83  					{
    84  						Module: "env", Name: internal.FunctionNotifyMemoryGrowth,
    85  						Type:     wasm.ExternTypeFunc,
    86  						DescFunc: 0,
    87  					},
    88  				},
    89  			},
    90  			expected: []*wasm.HostFunc{internal.NotifyMemoryGrowth},
    91  		},
    92  		{
    93  			name: "all result types",
    94  			input: &wasm.Module{
    95  				TypeSection: []wasm.FunctionType{
    96  					{Params: []wasm.ValueType{i32}},
    97  					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}},
    98  					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i64}},
    99  					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{f32}},
   100  					{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{f64}},
   101  				},
   102  				ImportSection: []wasm.Import{
   103  					{
   104  						Module: "env", Name: "invoke_v",
   105  						Type:     wasm.ExternTypeFunc,
   106  						DescFunc: 0,
   107  					},
   108  					{
   109  						Module: "env", Name: "invoke_i",
   110  						Type:     wasm.ExternTypeFunc,
   111  						DescFunc: 1,
   112  					},
   113  					{
   114  						Module: "env", Name: "invoke_p",
   115  						Type:     wasm.ExternTypeFunc,
   116  						DescFunc: 1,
   117  					},
   118  					{
   119  						Module: "env", Name: "invoke_j",
   120  						Type:     wasm.ExternTypeFunc,
   121  						DescFunc: 2,
   122  					},
   123  					{
   124  						Module: "env", Name: "invoke_f",
   125  						Type:     wasm.ExternTypeFunc,
   126  						DescFunc: 3,
   127  					},
   128  					{
   129  						Module: "env", Name: "invoke_d",
   130  						Type:     wasm.ExternTypeFunc,
   131  						DescFunc: 4,
   132  					},
   133  				},
   134  			},
   135  			expected: []*wasm.HostFunc{
   136  				{
   137  					ExportName: "invoke_v",
   138  					ParamTypes: []api.ValueType{i32},
   139  					ParamNames: []string{"index"},
   140  					Code:       wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}},
   141  				},
   142  				{
   143  					ExportName:  "invoke_i",
   144  					ParamTypes:  []api.ValueType{i32},
   145  					ParamNames:  []string{"index"},
   146  					ResultTypes: []api.ValueType{i32},
   147  					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{i32}}}},
   148  				},
   149  				{
   150  					ExportName:  "invoke_p",
   151  					ParamTypes:  []api.ValueType{i32},
   152  					ParamNames:  []string{"index"},
   153  					ResultTypes: []api.ValueType{i32},
   154  					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{i32}}}},
   155  				},
   156  				{
   157  					ExportName:  "invoke_j",
   158  					ParamTypes:  []api.ValueType{i32},
   159  					ParamNames:  []string{"index"},
   160  					ResultTypes: []api.ValueType{i64},
   161  					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{i64}}}},
   162  				},
   163  				{
   164  					ExportName:  "invoke_f",
   165  					ParamTypes:  []api.ValueType{i32},
   166  					ParamNames:  []string{"index"},
   167  					ResultTypes: []api.ValueType{f32},
   168  					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{f32}}}},
   169  				},
   170  				{
   171  					ExportName:  "invoke_d",
   172  					ParamTypes:  []api.ValueType{i32},
   173  					ParamNames:  []string{"index"},
   174  					ResultTypes: []api.ValueType{f64},
   175  					Code:        wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{f64}}}},
   176  				},
   177  			},
   178  		},
   179  		{
   180  			name: "ignores other imports",
   181  			input: &wasm.Module{
   182  				TypeSection: []wasm.FunctionType{
   183  					{Params: []wasm.ValueType{i32}},
   184  				},
   185  				ImportSection: []wasm.Import{
   186  					{
   187  						Module: "anv", Name: "invoke_v",
   188  						Type:     wasm.ExternTypeFunc,
   189  						DescFunc: 0,
   190  					},
   191  					{
   192  						Module: "env", Name: "invoke_v",
   193  						Type:     wasm.ExternTypeFunc,
   194  						DescFunc: 0,
   195  					},
   196  					{
   197  						Module: "env", Name: "grow",
   198  						Type:     wasm.ExternTypeFunc,
   199  						DescFunc: 0,
   200  					},
   201  				},
   202  			},
   203  			expected: []*wasm.HostFunc{
   204  				{
   205  					ExportName: "invoke_v",
   206  					ParamTypes: []api.ValueType{i32},
   207  					ParamNames: []string{"index"},
   208  					Code:       wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}},
   209  				},
   210  			},
   211  		},
   212  		{
   213  			name: "invoke_v and " + internal.FunctionNotifyMemoryGrowth,
   214  			input: &wasm.Module{
   215  				TypeSection: []wasm.FunctionType{{Params: []wasm.ValueType{i32}}},
   216  				ImportSection: []wasm.Import{
   217  					{
   218  						Module: "env", Name: "invoke_v",
   219  						Type:     wasm.ExternTypeFunc,
   220  						DescFunc: 0,
   221  					},
   222  					{
   223  						Module: "env", Name: internal.FunctionNotifyMemoryGrowth,
   224  						Type:     wasm.ExternTypeFunc,
   225  						DescFunc: 0,
   226  					},
   227  				},
   228  			},
   229  			expected: []*wasm.HostFunc{
   230  				{
   231  					ExportName: "invoke_v",
   232  					ParamTypes: []api.ValueType{i32},
   233  					ParamNames: []string{"index"},
   234  					Code:       wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}},
   235  				},
   236  				internal.NotifyMemoryGrowth,
   237  			},
   238  		},
   239  		{
   240  			name: "invoke_vi",
   241  			input: &wasm.Module{
   242  				TypeSection: []wasm.FunctionType{
   243  					{Params: []wasm.ValueType{i32, i32}},
   244  				},
   245  				ImportSection: []wasm.Import{
   246  					{
   247  						Module: "env", Name: "invoke_vi",
   248  						Type:     wasm.ExternTypeFunc,
   249  						DescFunc: 0,
   250  					},
   251  				},
   252  			},
   253  			expected: []*wasm.HostFunc{
   254  				{
   255  					ExportName: "invoke_vi",
   256  					ParamTypes: []api.ValueType{i32, i32},
   257  					ParamNames: []string{"index", "a1"},
   258  					Code:       wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Params: []api.ValueType{i32}}}},
   259  				},
   260  			},
   261  		},
   262  		{
   263  			name: "invoke_iiiii",
   264  			input: &wasm.Module{
   265  				TypeSection: []wasm.FunctionType{
   266  					{
   267  						Params:  []wasm.ValueType{i32, i32, i32, i32, i32},
   268  						Results: []wasm.ValueType{i32},
   269  					},
   270  				},
   271  				ImportSection: []wasm.Import{
   272  					{
   273  						Module: "env", Name: "invoke_iiiii",
   274  						Type:     wasm.ExternTypeFunc,
   275  						DescFunc: 0,
   276  					},
   277  				},
   278  			},
   279  			expected: []*wasm.HostFunc{
   280  				{
   281  					ExportName:  "invoke_iiiii",
   282  					ParamTypes:  []api.ValueType{i32, i32, i32, i32, i32},
   283  					ParamNames:  []string{"index", "a1", "a2", "a3", "a4"},
   284  					ResultTypes: []wasm.ValueType{i32},
   285  					Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{
   286  						Params:  []api.ValueType{i32, i32, i32, i32},
   287  						Results: []api.ValueType{i32},
   288  					}}},
   289  				},
   290  			},
   291  		},
   292  		{
   293  			name: "invoke_viiiddiiiiii",
   294  			input: &wasm.Module{
   295  				TypeSection: []wasm.FunctionType{
   296  					{
   297  						Params: []wasm.ValueType{i32, i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32},
   298  					},
   299  				},
   300  				ImportSection: []wasm.Import{
   301  					{
   302  						Module: "env", Name: "invoke_viiiddiiiiii",
   303  						Type:     wasm.ExternTypeFunc,
   304  						DescFunc: 0,
   305  					},
   306  				},
   307  			},
   308  			expected: []*wasm.HostFunc{
   309  				{
   310  					ExportName: "invoke_viiiddiiiiii",
   311  					ParamTypes: []api.ValueType{i32, i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32},
   312  					ParamNames: []string{"index", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10", "a11"},
   313  					Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{
   314  						Params: []api.ValueType{i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32},
   315  					}}},
   316  				},
   317  			},
   318  		},
   319  	}
   320  
   321  	for _, tt := range tests {
   322  		tc := tt
   323  
   324  		t.Run(tc.name, func(t *testing.T) {
   325  			r := wazero.NewRuntime(testCtx)
   326  			defer r.Close(testCtx)
   327  
   328  			guest, err := r.CompileModule(testCtx, binaryencoding.EncodeModule(tc.input))
   329  			require.NoError(t, err)
   330  
   331  			exporter, err := NewFunctionExporterForModule(guest)
   332  			require.NoError(t, err)
   333  			actual := exporter.(emscriptenFns)
   334  
   335  			require.Equal(t, len(tc.expected), len(actual))
   336  			for i, expected := range tc.expected {
   337  				require.Equal(t, expected, actual[i], actual[i].ExportName)
   338  			}
   339  		})
   340  	}
   341  }
   342  
   343  func TestInstantiateForModule(t *testing.T) {
   344  	var log bytes.Buffer
   345  
   346  	// Set context to one that has an experimental listener
   347  	ctx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(&log))
   348  
   349  	r := wazero.NewRuntime(ctx)
   350  	defer r.Close(ctx)
   351  
   352  	compiled, err := r.CompileModule(ctx, invokeWasm)
   353  	require.NoError(t, err)
   354  
   355  	_, err = InstantiateForModule(ctx, r, compiled)
   356  	require.NoError(t, err)
   357  
   358  	mod, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig())
   359  	require.NoError(t, err)
   360  
   361  	tests := []struct {
   362  		name, funcName          string
   363  		tableOffset             int
   364  		params, expectedResults []uint64
   365  		expectedLog             string
   366  	}{
   367  		{
   368  			name:            "invoke_i",
   369  			funcName:        "call_v_i32",
   370  			expectedResults: []uint64{42},
   371  			expectedLog: `--> .call_v_i32(0)
   372  	==> env.invoke_i(index=0)
   373  		--> .stackSave()
   374  		<-- 65536
   375  		--> .v_i32()
   376  		<-- 42
   377  	<== 42
   378  <-- 42
   379  `,
   380  		},
   381  		{
   382  			name:            "invoke_ii",
   383  			funcName:        "call_i32_i32",
   384  			tableOffset:     2,
   385  			params:          []uint64{42},
   386  			expectedResults: []uint64{42},
   387  			expectedLog: `--> .call_i32_i32(2,42)
   388  	==> env.invoke_ii(index=2,a1=42)
   389  		--> .stackSave()
   390  		<-- 65536
   391  		--> .i32_i32(42)
   392  		<-- 42
   393  	<== 42
   394  <-- 42
   395  `,
   396  		},
   397  		{
   398  			name:            "invoke_iii",
   399  			funcName:        "call_i32i32_i32",
   400  			tableOffset:     4,
   401  			params:          []uint64{1, 2},
   402  			expectedResults: []uint64{3},
   403  			expectedLog: `--> .call_i32i32_i32(4,1,2)
   404  	==> env.invoke_iii(index=4,a1=1,a2=2)
   405  		--> .stackSave()
   406  		<-- 65536
   407  		--> .i32i32_i32(1,2)
   408  		<-- 3
   409  	<== 3
   410  <-- 3
   411  `,
   412  		},
   413  		{
   414  			name:            "invoke_iiii",
   415  			funcName:        "call_i32i32i32_i32",
   416  			tableOffset:     6,
   417  			params:          []uint64{1, 2, 4},
   418  			expectedResults: []uint64{7},
   419  			expectedLog: `--> .call_i32i32i32_i32(6,1,2,4)
   420  	==> env.invoke_iiii(index=6,a1=1,a2=2,a3=4)
   421  		--> .stackSave()
   422  		<-- 65536
   423  		--> .i32i32i32_i32(1,2,4)
   424  		<-- 7
   425  	<== 7
   426  <-- 7
   427  `,
   428  		},
   429  		{
   430  			name:            "invoke_iiiii",
   431  			funcName:        "calli32_i32i32i32i32_i32",
   432  			tableOffset:     8,
   433  			params:          []uint64{1, 2, 4, 8},
   434  			expectedResults: []uint64{15},
   435  			expectedLog: `--> .calli32_i32i32i32i32_i32(8,1,2,4,8)
   436  	==> env.invoke_iiiii(index=8,a1=1,a2=2,a3=4,a4=8)
   437  		--> .stackSave()
   438  		<-- 65536
   439  		--> .i32i32i32i32_i32(1,2,4,8)
   440  		<-- 15
   441  	<== 15
   442  <-- 15
   443  `,
   444  		},
   445  		{
   446  			name:        "invoke_v",
   447  			funcName:    "call_v_v",
   448  			tableOffset: 10,
   449  			expectedLog: `--> .call_v_v(10)
   450  	==> env.invoke_v(index=10)
   451  		--> .stackSave()
   452  		<-- 65536
   453  		--> .v_v()
   454  		<--
   455  	<==
   456  <--
   457  `,
   458  		},
   459  		{
   460  			name:        "invoke_vi",
   461  			funcName:    "call_i32_v",
   462  			tableOffset: 12,
   463  			params:      []uint64{42},
   464  			expectedLog: `--> .call_i32_v(12,42)
   465  	==> env.invoke_vi(index=12,a1=42)
   466  		--> .stackSave()
   467  		<-- 65536
   468  		--> .i32_v(42)
   469  		<--
   470  	<==
   471  <--
   472  `,
   473  		},
   474  		{
   475  			name:        "invoke_vii",
   476  			funcName:    "call_i32i32_v",
   477  			tableOffset: 14,
   478  			params:      []uint64{1, 2},
   479  			expectedLog: `--> .call_i32i32_v(14,1,2)
   480  	==> env.invoke_vii(index=14,a1=1,a2=2)
   481  		--> .stackSave()
   482  		<-- 65536
   483  		--> .i32i32_v(1,2)
   484  		<--
   485  	<==
   486  <--
   487  `,
   488  		},
   489  		{
   490  			name:        "invoke_viii",
   491  			funcName:    "call_i32i32i32_v",
   492  			tableOffset: 16,
   493  			params:      []uint64{1, 2, 4},
   494  			expectedLog: `--> .call_i32i32i32_v(16,1,2,4)
   495  	==> env.invoke_viii(index=16,a1=1,a2=2,a3=4)
   496  		--> .stackSave()
   497  		<-- 65536
   498  		--> .i32i32i32_v(1,2,4)
   499  		<--
   500  	<==
   501  <--
   502  `,
   503  		},
   504  		{
   505  			name:        "invoke_viiii",
   506  			funcName:    "calli32_i32i32i32i32_v",
   507  			tableOffset: 18,
   508  			params:      []uint64{1, 2, 4, 8},
   509  			expectedLog: `--> .calli32_i32i32i32i32_v(18,1,2,4,8)
   510  	==> env.invoke_viiii(index=18,a1=1,a2=2,a3=4,a4=8)
   511  		--> .stackSave()
   512  		<-- 65536
   513  		--> .i32i32i32i32_v(1,2,4,8)
   514  		<--
   515  	<==
   516  <--
   517  `,
   518  		},
   519  		{
   520  			name:        "invoke_v_with_longjmp",
   521  			funcName:    "call_invoke_v_with_longjmp_throw",
   522  			tableOffset: 20,
   523  			params:      []uint64{},
   524  			expectedLog: `--> .call_invoke_v_with_longjmp_throw(20)
   525  	==> env.invoke_v(index=20)
   526  		--> .stackSave()
   527  		<-- 42
   528  		--> .call_longjmp_throw()
   529  			==> env._emscripten_throw_longjmp()
   530  		--> .stackRestore(42)
   531  		<--
   532  		--> .setThrew(1,0)
   533  		<--
   534  	<==
   535  <--
   536  `,
   537  		},
   538  	}
   539  
   540  	for _, tt := range tests {
   541  		tc := tt
   542  
   543  		t.Run(tc.name, func(t *testing.T) {
   544  			defer log.Reset()
   545  
   546  			params := tc.params
   547  			params = append([]uint64{uint64(tc.tableOffset)}, params...)
   548  
   549  			results, err := mod.ExportedFunction(tc.funcName).Call(testCtx, params...)
   550  			require.NoError(t, err)
   551  			require.Equal(t, tc.expectedResults, results)
   552  
   553  			// We expect to see the dynamic function call target
   554  			require.Equal(t, tc.expectedLog, log.String())
   555  
   556  			// We expect an unreachable function to err
   557  			params[0]++
   558  			_, err = mod.ExportedFunction(tc.funcName).Call(testCtx, params...)
   559  			require.Error(t, err)
   560  		})
   561  	}
   562  }