github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/builder_test.go (about)

     1  package wazero
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  
     7  	"github.com/bananabytelabs/wazero/api"
     8  	"github.com/bananabytelabs/wazero/internal/testing/require"
     9  	"github.com/bananabytelabs/wazero/internal/wasm"
    10  )
    11  
    12  // TestNewHostModuleBuilder_Compile only covers a few scenarios to avoid duplicating tests in internal/wasm/host_test.go
    13  func TestNewHostModuleBuilder_Compile(t *testing.T) {
    14  	i32, i64 := api.ValueTypeI32, api.ValueTypeI64
    15  
    16  	uint32_uint32 := func(context.Context, uint32) uint32 {
    17  		return 0
    18  	}
    19  	uint64_uint32 := func(context.Context, uint64) uint32 {
    20  		return 0
    21  	}
    22  
    23  	gofunc1 := api.GoFunc(func(ctx context.Context, stack []uint64) {
    24  		stack[0] = 0
    25  	})
    26  	gofunc2 := api.GoFunc(func(ctx context.Context, stack []uint64) {
    27  		stack[0] = 0
    28  	})
    29  
    30  	tests := []struct {
    31  		name     string
    32  		input    func(Runtime) HostModuleBuilder
    33  		expected *wasm.Module
    34  	}{
    35  		{
    36  			name: "empty",
    37  			input: func(r Runtime) HostModuleBuilder {
    38  				return r.NewHostModuleBuilder("host")
    39  			},
    40  			expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "host"}},
    41  		},
    42  		{
    43  			name: "only name",
    44  			input: func(r Runtime) HostModuleBuilder {
    45  				return r.NewHostModuleBuilder("env")
    46  			},
    47  			expected: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "env"}},
    48  		},
    49  		{
    50  			name: "WithFunc",
    51  			input: func(r Runtime) HostModuleBuilder {
    52  				return r.NewHostModuleBuilder("host").
    53  					NewFunctionBuilder().WithFunc(uint32_uint32).Export("1")
    54  			},
    55  			expected: &wasm.Module{
    56  				TypeSection: []wasm.FunctionType{
    57  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
    58  				},
    59  				FunctionSection: []wasm.Index{0},
    60  				CodeSection:     []wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
    61  				ExportSection: []wasm.Export{
    62  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
    63  				},
    64  				Exports: map[string]*wasm.Export{
    65  					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
    66  				},
    67  				NameSection: &wasm.NameSection{
    68  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
    69  					ModuleName:    "host",
    70  				},
    71  			},
    72  		},
    73  		{
    74  			name: "WithFunc WithName WithParameterNames",
    75  			input: func(r Runtime) HostModuleBuilder {
    76  				return r.NewHostModuleBuilder("host").NewFunctionBuilder().
    77  					WithFunc(uint32_uint32).
    78  					WithName("get").WithParameterNames("x").
    79  					Export("1")
    80  			},
    81  			expected: &wasm.Module{
    82  				TypeSection: []wasm.FunctionType{
    83  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
    84  				},
    85  				FunctionSection: []wasm.Index{0},
    86  				CodeSection:     []wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
    87  				ExportSection: []wasm.Export{
    88  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
    89  				},
    90  				Exports: map[string]*wasm.Export{
    91  					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
    92  				},
    93  				NameSection: &wasm.NameSection{
    94  					FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
    95  					LocalNames:    []wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
    96  					ModuleName:    "host",
    97  				},
    98  			},
    99  		},
   100  		{
   101  			name: "WithFunc WithName WithResultNames",
   102  			input: func(r Runtime) HostModuleBuilder {
   103  				return r.NewHostModuleBuilder("host").NewFunctionBuilder().
   104  					WithFunc(uint32_uint32).
   105  					WithName("get").WithResultNames("x").
   106  					Export("1")
   107  			},
   108  			expected: &wasm.Module{
   109  				TypeSection: []wasm.FunctionType{
   110  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
   111  				},
   112  				FunctionSection: []wasm.Index{0},
   113  				CodeSection:     []wasm.Code{wasm.MustParseGoReflectFuncCode(uint32_uint32)},
   114  				ExportSection: []wasm.Export{
   115  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   116  				},
   117  				Exports: map[string]*wasm.Export{
   118  					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   119  				},
   120  				NameSection: &wasm.NameSection{
   121  					FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
   122  					ResultNames:   []wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
   123  					ModuleName:    "host",
   124  				},
   125  			},
   126  		},
   127  		{
   128  			name: "WithFunc overwrites existing",
   129  			input: func(r Runtime) HostModuleBuilder {
   130  				return r.NewHostModuleBuilder("host").
   131  					NewFunctionBuilder().WithFunc(uint32_uint32).Export("1").
   132  					NewFunctionBuilder().WithFunc(uint64_uint32).Export("1")
   133  			},
   134  			expected: &wasm.Module{
   135  				TypeSection: []wasm.FunctionType{
   136  					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
   137  				},
   138  				FunctionSection: []wasm.Index{0},
   139  				CodeSection:     []wasm.Code{wasm.MustParseGoReflectFuncCode(uint64_uint32)},
   140  				ExportSection: []wasm.Export{
   141  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   142  				},
   143  				Exports: map[string]*wasm.Export{
   144  					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   145  				},
   146  				NameSection: &wasm.NameSection{
   147  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
   148  					ModuleName:    "host",
   149  				},
   150  			},
   151  		},
   152  		{
   153  			name: "WithFunc twice",
   154  			input: func(r Runtime) HostModuleBuilder {
   155  				// Intentionally out of order
   156  				return r.NewHostModuleBuilder("host").
   157  					NewFunctionBuilder().WithFunc(uint64_uint32).Export("2").
   158  					NewFunctionBuilder().WithFunc(uint32_uint32).Export("1")
   159  			},
   160  			expected: &wasm.Module{
   161  				TypeSection: []wasm.FunctionType{
   162  					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
   163  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
   164  				},
   165  				FunctionSection: []wasm.Index{0, 1},
   166  				CodeSection:     []wasm.Code{wasm.MustParseGoReflectFuncCode(uint64_uint32), wasm.MustParseGoReflectFuncCode(uint32_uint32)},
   167  				ExportSection: []wasm.Export{
   168  					{Name: "2", Type: wasm.ExternTypeFunc, Index: 0},
   169  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 1},
   170  				},
   171  				Exports: map[string]*wasm.Export{
   172  					"2": {Name: "2", Type: wasm.ExternTypeFunc, Index: 0},
   173  					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 1},
   174  				},
   175  				NameSection: &wasm.NameSection{
   176  					FunctionNames: wasm.NameMap{{Index: 0, Name: "2"}, {Index: 1, Name: "1"}},
   177  					ModuleName:    "host",
   178  				},
   179  			},
   180  		},
   181  		{
   182  			name: "WithGoFunction",
   183  			input: func(r Runtime) HostModuleBuilder {
   184  				return r.NewHostModuleBuilder("host").
   185  					NewFunctionBuilder().
   186  					WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
   187  					Export("1")
   188  			},
   189  			expected: &wasm.Module{
   190  				TypeSection: []wasm.FunctionType{
   191  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
   192  				},
   193  				FunctionSection: []wasm.Index{0},
   194  				CodeSection: []wasm.Code{
   195  					{GoFunc: gofunc1},
   196  				},
   197  				ExportSection: []wasm.Export{
   198  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   199  				},
   200  				Exports: map[string]*wasm.Export{
   201  					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   202  				},
   203  				NameSection: &wasm.NameSection{
   204  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
   205  					ModuleName:    "host",
   206  				},
   207  			},
   208  		},
   209  		{
   210  			name: "WithGoFunction WithName WithParameterNames",
   211  			input: func(r Runtime) HostModuleBuilder {
   212  				return r.NewHostModuleBuilder("host").NewFunctionBuilder().
   213  					WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
   214  					WithName("get").WithParameterNames("x").
   215  					Export("1")
   216  			},
   217  			expected: &wasm.Module{
   218  				TypeSection: []wasm.FunctionType{
   219  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
   220  				},
   221  				FunctionSection: []wasm.Index{0},
   222  				CodeSection: []wasm.Code{
   223  					{GoFunc: gofunc1},
   224  				},
   225  				ExportSection: []wasm.Export{
   226  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   227  				},
   228  				Exports: map[string]*wasm.Export{
   229  					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   230  				},
   231  				NameSection: &wasm.NameSection{
   232  					FunctionNames: wasm.NameMap{{Index: 0, Name: "get"}},
   233  					LocalNames:    []wasm.NameMapAssoc{{Index: 0, NameMap: wasm.NameMap{{Index: 0, Name: "x"}}}},
   234  					ModuleName:    "host",
   235  				},
   236  			},
   237  		},
   238  		{
   239  			name: "WithGoFunction overwrites existing",
   240  			input: func(r Runtime) HostModuleBuilder {
   241  				return r.NewHostModuleBuilder("host").
   242  					NewFunctionBuilder().
   243  					WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
   244  					Export("1").
   245  					NewFunctionBuilder().
   246  					WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}).
   247  					Export("1")
   248  			},
   249  			expected: &wasm.Module{
   250  				TypeSection: []wasm.FunctionType{
   251  					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
   252  				},
   253  				FunctionSection: []wasm.Index{0},
   254  				CodeSection: []wasm.Code{
   255  					{GoFunc: gofunc2},
   256  				},
   257  				ExportSection: []wasm.Export{
   258  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   259  				},
   260  				Exports: map[string]*wasm.Export{
   261  					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 0},
   262  				},
   263  				NameSection: &wasm.NameSection{
   264  					FunctionNames: wasm.NameMap{{Index: 0, Name: "1"}},
   265  					ModuleName:    "host",
   266  				},
   267  			},
   268  		},
   269  		{
   270  			name: "WithGoFunction twice",
   271  			input: func(r Runtime) HostModuleBuilder {
   272  				// Intentionally not in lexicographic order
   273  				return r.NewHostModuleBuilder("host").
   274  					NewFunctionBuilder().
   275  					WithGoFunction(gofunc2, []api.ValueType{i64}, []api.ValueType{i32}).
   276  					Export("2").
   277  					NewFunctionBuilder().
   278  					WithGoFunction(gofunc1, []api.ValueType{i32}, []api.ValueType{i32}).
   279  					Export("1")
   280  			},
   281  			expected: &wasm.Module{
   282  				TypeSection: []wasm.FunctionType{
   283  					{Params: []api.ValueType{i64}, Results: []api.ValueType{i32}},
   284  					{Params: []api.ValueType{i32}, Results: []api.ValueType{i32}},
   285  				},
   286  				FunctionSection: []wasm.Index{0, 1},
   287  				CodeSection: []wasm.Code{
   288  					{GoFunc: gofunc2},
   289  					{GoFunc: gofunc1},
   290  				},
   291  				ExportSection: []wasm.Export{
   292  					{Name: "2", Type: wasm.ExternTypeFunc, Index: 0},
   293  					{Name: "1", Type: wasm.ExternTypeFunc, Index: 1},
   294  				},
   295  				Exports: map[string]*wasm.Export{
   296  					"2": {Name: "2", Type: wasm.ExternTypeFunc, Index: 0},
   297  					"1": {Name: "1", Type: wasm.ExternTypeFunc, Index: 1},
   298  				},
   299  				NameSection: &wasm.NameSection{
   300  					FunctionNames: wasm.NameMap{{Index: 0, Name: "2"}, {Index: 1, Name: "1"}},
   301  					ModuleName:    "host",
   302  				},
   303  			},
   304  		},
   305  	}
   306  
   307  	for _, tt := range tests {
   308  		tc := tt
   309  
   310  		t.Run(tc.name, func(t *testing.T) {
   311  			b := tc.input(NewRuntime(testCtx)).(*hostModuleBuilder)
   312  			compiled, err := b.Compile(testCtx)
   313  			require.NoError(t, err)
   314  			m := compiled.(*compiledModule)
   315  
   316  			requireHostModuleEquals(t, tc.expected, m.module)
   317  
   318  			require.Equal(t, b.r.store.Engine, m.compiledEngine)
   319  
   320  			// TypeIDs must be assigned to compiledModule.
   321  			expTypeIDs, err := b.r.store.GetFunctionTypeIDs(tc.expected.TypeSection)
   322  			require.NoError(t, err)
   323  			require.Equal(t, expTypeIDs, m.typeIDs)
   324  
   325  			// Built module must be instantiable by Engine.
   326  			mod, err := b.r.InstantiateModule(testCtx, m, NewModuleConfig())
   327  			require.NoError(t, err)
   328  
   329  			// Closing the module shouldn't remove the compiler cache
   330  			require.NoError(t, mod.Close(testCtx))
   331  			require.Equal(t, uint32(1), b.r.store.Engine.CompiledModuleCount())
   332  		})
   333  	}
   334  }
   335  
   336  // TestNewHostModuleBuilder_Compile_Errors only covers a few scenarios to avoid
   337  // duplicating tests in internal/wasm/host_test.go
   338  func TestNewHostModuleBuilder_Compile_Errors(t *testing.T) {
   339  	tests := []struct {
   340  		name        string
   341  		input       func(Runtime) HostModuleBuilder
   342  		expectedErr string
   343  	}{
   344  		{
   345  			name: "error compiling", // should fail due to invalid param.
   346  			input: func(rt Runtime) HostModuleBuilder {
   347  				return rt.NewHostModuleBuilder("host").NewFunctionBuilder().
   348  					WithFunc(&wasm.HostFunc{ExportName: "fn", Code: wasm.Code{GoFunc: func(string) {}}}).
   349  					Export("fn")
   350  			},
   351  			expectedErr: `func[host.fn] param[0] is unsupported: string`,
   352  		},
   353  	}
   354  
   355  	for _, tt := range tests {
   356  		tc := tt
   357  
   358  		t.Run(tc.name, func(t *testing.T) {
   359  			_, e := tc.input(NewRuntime(testCtx)).Compile(testCtx)
   360  			require.EqualError(t, e, tc.expectedErr)
   361  		})
   362  	}
   363  }
   364  
   365  // TestNewHostModuleBuilder_Instantiate ensures Runtime.InstantiateModule is called on success.
   366  func TestNewHostModuleBuilder_Instantiate(t *testing.T) {
   367  	r := NewRuntime(testCtx)
   368  	m, err := r.NewHostModuleBuilder("env").Instantiate(testCtx)
   369  	require.NoError(t, err)
   370  
   371  	// If this was instantiated, it would be added to the store under the same name
   372  	require.Equal(t, r.Module("env"), m)
   373  
   374  	// Closing the module should remove the compiler cache
   375  	require.NoError(t, m.Close(testCtx))
   376  	require.Zero(t, r.(*runtime).store.Engine.CompiledModuleCount())
   377  }
   378  
   379  // TestNewHostModuleBuilder_Instantiate_Errors ensures errors propagate from Runtime.InstantiateModule
   380  func TestNewHostModuleBuilder_Instantiate_Errors(t *testing.T) {
   381  	r := NewRuntime(testCtx)
   382  	_, err := r.NewHostModuleBuilder("env").Instantiate(testCtx)
   383  	require.NoError(t, err)
   384  
   385  	_, err = r.NewHostModuleBuilder("env").Instantiate(testCtx)
   386  	require.EqualError(t, err, "module[env] has already been instantiated")
   387  }
   388  
   389  // requireHostModuleEquals is redefined from internal/wasm/host_test.go to avoid an import cycle extracting it.
   390  func requireHostModuleEquals(t *testing.T, expected, actual *wasm.Module) {
   391  	// `require.Equal(t, expected, actual)` fails reflect pointers don't match, so brute compare:
   392  	for i := range expected.TypeSection {
   393  		tp := &expected.TypeSection[i]
   394  		tp.CacheNumInUint64()
   395  		// When creating the compiled module, we get the type IDs for types, which results in caching type keys.
   396  		_ = tp.String()
   397  	}
   398  	require.Equal(t, expected.TypeSection, actual.TypeSection)
   399  	require.Equal(t, expected.ImportSection, actual.ImportSection)
   400  	require.Equal(t, expected.FunctionSection, actual.FunctionSection)
   401  	require.Equal(t, expected.TableSection, actual.TableSection)
   402  	require.Equal(t, expected.MemorySection, actual.MemorySection)
   403  	require.Equal(t, expected.GlobalSection, actual.GlobalSection)
   404  	require.Equal(t, expected.ExportSection, actual.ExportSection)
   405  	require.Equal(t, expected.Exports, actual.Exports)
   406  	require.Equal(t, expected.StartSection, actual.StartSection)
   407  	require.Equal(t, expected.ElementSection, actual.ElementSection)
   408  	require.Equal(t, expected.DataSection, actual.DataSection)
   409  	require.Equal(t, expected.NameSection, actual.NameSection)
   410  
   411  	// Special case because reflect.Value can't be compared with Equals
   412  	// TODO: This is copy/paste with /internal/wasm/host_test.go
   413  	require.Equal(t, len(expected.CodeSection), len(actual.CodeSection))
   414  	for i, c := range expected.CodeSection {
   415  		actualCode := actual.CodeSection[i]
   416  		require.Equal(t, c.GoFunc, actualCode.GoFunc)
   417  
   418  		// Not wasm
   419  		require.Nil(t, actualCode.Body)
   420  		require.Nil(t, actualCode.LocalTypes)
   421  	}
   422  }