/src/wasm3/source/m3_host.h
Line | Count | Source |
1 | | // |
2 | | // m3_host.h |
3 | | // |
4 | | // What the engine asks of the system it is hosted on. |
5 | | // |
6 | | // Everything here is something only the operating system can answer: how much native |
7 | | // stack this thread still has, how to get at a file's bytes, and - where the build |
8 | | // asks for guarded memories - how to reserve address space, commit part of it, and |
9 | | // turn the fault from a read past the committed part back into a Wasm trap. |
10 | | // |
11 | | // Exactly one implementation is ever built: |
12 | | // |
13 | | // m3_host_win32.h VirtualAlloc/VirtualQuery, file mappings, and structured |
14 | | // exception handling |
15 | | // m3_host_posix.h mmap/mprotect, file mappings, and a SIGSEGV handler |
16 | | // m3_host_none.h a system with none of that, which is most of platforms/: |
17 | | // it reads a file rather than mapping one, and says it cannot |
18 | | // measure a stack |
19 | | // |
20 | | // All three are headers, and m3_core.c includes whichever one it is built for: a new |
21 | | // .c would have to be added to every source list that names its sources one by one, |
22 | | // and most of those are build scripts outside this repository. Unlike the WASI host |
23 | | // split, the callers here are in other translation units, so these are real |
24 | | // definitions rather than static ones - this header is the only declaration of them |
25 | | // there is. |
26 | | // |
27 | | // Every call is allowed to fail, and failing is not an error: m3_HostStackBase |
28 | | // answers NULL on a system that cannot be asked, and the engine falls back to the |
29 | | // compile-time budget. Only the reservation calls report failure the caller has to |
30 | | // act on, and only a build with d_m3GuardedMemory has any. |
31 | | // |
32 | | |
33 | | #ifndef m3_host_h |
34 | | #define m3_host_h |
35 | | |
36 | | #include "m3_core.h" |
37 | | |
38 | | d_m3BeginExternC |
39 | | |
40 | | // The lowest address the calling thread's stack can reach, or NULL when this |
41 | | // build cannot ask (see d_m3HasThreadStackProbe). What sits at the very bottom |
42 | | // is the system's business - a guard page usually - so the answer bounds the |
43 | | // stack rather than describing what is safe to touch; d_m3NativeStackMargin is |
44 | | // what keeps the difference. |
45 | | void* m3_HostStackBase (void); |
46 | | |
47 | | |
48 | | // A file's bytes and how they were come by. |
49 | | // |
50 | | // 'mapped' is the one that says who gives the bytes back, and it is not the same |
51 | | // question as whether there is a handle: a Win32 mapping keeps nothing open, because |
52 | | // the view holds the section which holds the file object and the share mode with it, |
53 | | // while POSIX has to hold the descriptor its lock lives on. So a mapped file may have |
54 | | // no handle, and d_m3HostNoHandle is what that looks like. |
55 | 0 | #define d_m3HostNoHandle ((intptr_t) -1) |
56 | | |
57 | | typedef struct M3HostFile { |
58 | | void* data; |
59 | | size_t size; |
60 | | intptr_t handle; // the host layer's, and d_m3HostNoHandle when there is none |
61 | | bool mapped; |
62 | | } M3HostFile; |
63 | | |
64 | | // The module's bytes, however this system can best get at them: mapped where it can |
65 | | // map, read onto the heap where it cannot - a system with no mapping at all, a |
66 | | // filesystem that will not, or something that is not a file, which is what a pipe |
67 | | // from a process substitution is. The caller is told which only by 'mapped', and |
68 | | // needs to know only because m3_HostUnmapFile is what gives the bytes back either |
69 | | // way. |
70 | | // |
71 | | // i_maxBytes refuses a file larger than the caller is willing to take, before |
72 | | // anything is allocated for it; 0 accepts any size the system will hand over. False |
73 | | // is that, or the file not being readable at all, and o_file is written on success |
74 | | // only. |
75 | | bool m3_HostMapFile (const char* i_path, size_t i_maxBytes, M3HostFile* o_file); |
76 | | |
77 | | // Give the bytes back, releasing the mapping and its lock, or the heap block |
78 | | void m3_HostUnmapFile (M3HostFile* io_file); |
79 | | |
80 | | // The reading half on its own: what each m3_HostMapFile falls back to, and the whole |
81 | | // of what a system with no mapping does. The same on every system, so it is here |
82 | | // rather than in any of them - and inline, so that a translation unit which never |
83 | | // calls it is not the one that drags stdio's FILE machinery into the link. |
84 | | static inline |
85 | | bool m3_HostReadFile (const char* i_path, size_t i_maxBytes, M3HostFile* o_file) |
86 | 0 | { |
87 | 0 | FILE* f = fopen(i_path, "rb"); |
88 | 0 | if (f == NULL) { |
89 | 0 | return false; |
90 | 0 | } |
91 | | |
92 | | // The size is taken before anything is allocated, so that a file too big to be |
93 | | // wanted is refused rather than read |
94 | 0 | fseek(f, 0, SEEK_END); |
95 | 0 | long tell = ftell(f); |
96 | 0 | fseek(f, 0, SEEK_SET); |
97 | |
|
98 | 0 | if (tell < 0 or (i_maxBytes and (size_t) tell > i_maxBytes)) { |
99 | 0 | fclose(f); |
100 | 0 | return false; |
101 | 0 | } |
102 | | |
103 | 0 | size_t size = (size_t)tell; |
104 | 0 | void* data = m3_Malloc("Host File", size ? size : 1); |
105 | |
|
106 | 0 | if (data == NULL) { |
107 | 0 | fclose(f); |
108 | 0 | return false; |
109 | 0 | } |
110 | | |
111 | 0 | bool read = (fread(data, 1, size, f) == size); |
112 | 0 | fclose(f); |
113 | |
|
114 | 0 | if (not read) { |
115 | 0 | m3_Free(data); |
116 | 0 | return false; |
117 | 0 | } |
118 | | |
119 | 0 | o_file->data = data; |
120 | 0 | o_file->size = size; |
121 | 0 | o_file->handle = d_m3HostNoHandle; |
122 | 0 | o_file->mapped = false; |
123 | | return true; |
124 | 0 | } Unexecuted instantiation: m3_env.c:m3_HostReadFile Unexecuted instantiation: m3_core.c:m3_HostReadFile |
125 | | |
126 | | // A module is parsed in place and keeps pointing into the mapped bytes for as long |
127 | | // as it is loaded, so this is where a module's own size stops being paid for twice: |
128 | | // what is mapped stays in the page cache, shared with anything else running the same |
129 | | // file, and is only ever read as far as the module is walked. |
130 | | // |
131 | | // What is mapped is the file itself, so a file that changes underneath the mapping |
132 | | // changes under a running module. Each system is asked to prevent that as far as it |
133 | | // can, and the two can do very different amounts: |
134 | | // |
135 | | // Win32 a share mode the kernel enforces. Another process opening the file for |
136 | | // writing is refused outright while the mapping lives, so this is a real |
137 | | // guarantee. Other readers - another wasm3 on the same module - are fine. |
138 | | // POSIX a shared advisory lock, which stops only a writer that asks for a lock |
139 | | // of its own. Nothing stops a plain open-and-write, because POSIX has no |
140 | | // portable mandatory locking: Linux removed what it had in 5.15, and |
141 | | // O_EXLOCK is a BSD extension that is advisory as well. |
142 | | // |
143 | | // So on POSIX this narrows the window rather than closing it. |
144 | | |
145 | | #if d_m3GuardedMemory |
146 | | |
147 | | # if M3_SIZEOF_PTR < 8 |
148 | | # error "d_m3GuardedMemory needs a 64-bit address space" |
149 | | # endif |
150 | | # if d_m3FixedHeap |
151 | | # error "d_m3GuardedMemory and d_m3FixedHeap are two different places for a linear memory to come from" |
152 | | # endif |
153 | | # if defined(_WIN32) && !defined(_MSC_VER) |
154 | | # error "d_m3GuardedMemory on Windows needs __try/__except - build with MSVC or clang-cl" |
155 | | # endif |
156 | | |
157 | | // The system's allocation granularity: what a reservation's size and a commit's |
158 | | // bounds are rounded to. Never zero. |
159 | | size_t m3_HostPageSize (void); |
160 | | |
161 | | // Reserve i_bytes of address space without backing any of it. Reading or writing it |
162 | | // faults until m3_HostCommit says otherwise, which is the whole point: the fault is |
163 | | // what a Wasm access past the end of its memory turns into. NULL if the system will |
164 | | // not. |
165 | | void* m3_HostReserve (size_t i_bytes); |
166 | | |
167 | | // Back [i_address, i_address + i_bytes) with pages, which read as zero. The range |
168 | | // has to be inside a reservation from m3_HostReserve; committing what is already |
169 | | // committed is not an error and changes nothing. |
170 | | bool m3_HostCommit (void* i_address, size_t i_bytes); |
171 | | |
172 | | // Undo m3_HostCommit for [i_address, i_address + i_bytes), giving the pages back to |
173 | | // the system and leaving the range reserved and unreachable again. What was written |
174 | | // there is gone: committing it once more reads as zero. |
175 | | bool m3_HostDecommit (void* i_address, size_t i_bytes); |
176 | | |
177 | | // Give back a whole reservation. i_bytes is what was reserved. |
178 | | void m3_HostRelease (void* i_address, size_t i_bytes); |
179 | | |
180 | | // Run i_body(i_context) with a fault inside [i_guardLow, i_guardLow + i_guardBytes) |
181 | | // caught and reported rather than killing the process: false means the body touched |
182 | | // that range and did not finish. Any other fault is left to whatever would have |
183 | | // handled it, so an engine bug still looks like an engine bug. |
184 | | // |
185 | | // Nests, so a host function calling back into Wasm gets a region of its own, and is |
186 | | // per-thread. What the body was doing when it faulted is abandoned where it stands - |
187 | | // the caller has to be able to carry on without it, which for the interpreter means |
188 | | // the Wasm stack and the memory, both of which are just data. |
189 | | bool m3_HostProtectedCall (void (*i_body)(void*), void* i_context, |
190 | | void* i_guardLow, size_t i_guardBytes); |
191 | | |
192 | | #endif // d_m3GuardedMemory |
193 | | |
194 | | d_m3EndExternC |
195 | | |
196 | | #endif // m3_host_h |