Line | Branch | Decision | Exec | Source |
---|---|---|---|---|
1 | #include "pch.h" | |||
2 | ||||
3 | #include "rusefi_lua.h" | |||
4 | #include "thread_controller.h" | |||
5 | ||||
6 | #if EFI_LUA | |||
7 | ||||
8 | #include "lua.hpp" | |||
9 | #include "lua_heap.h" | |||
10 | #include "lua_hooks.h" | |||
11 | #include "can_filter.h" | |||
12 | ||||
13 | #define TAG "LUA " | |||
14 | ||||
15 | static bool withErrorLoading = false; | |||
16 | static int luaTickPeriodUs; | |||
17 | ||||
18 | #if EFI_CAN_SUPPORT | |||
19 | static int recentRxCount = 0; | |||
20 | static int totalRxCount = 0; | |||
21 | static int rxTime; | |||
22 | #endif // EFI_CAN_SUPPORT | |||
23 | ||||
24 | ✗ | static int lua_setTickRate(lua_State* l) { | ||
25 | ✗ | float userFreq = luaL_checknumber(l, 1); | ||
26 | ||||
27 | // For instance BMW does 100 CAN messages per second on some IDs, let's allow at least twice that speed | |||
28 | // Limit to 1..200 hz | |||
29 | ✗ | float freq = clampF(1, userFreq, 2000); | ||
30 | ✗ | if (freq != userFreq) { | ||
31 | ✗ | efiPrintf(TAG "clamping tickrate %f", freq); | ||
32 | } | |||
33 | ||||
34 | ✗ | if (freq > 150 && !engineConfiguration->luaCanRxWorkaround) { | ||
35 | ✗ | efiPrintf(TAG "luaCanRxWorkaround recommended at high tick rate!"); | ||
36 | } | |||
37 | ||||
38 | ✗ | luaTickPeriodUs = 1000000.0f / freq; | ||
39 | ✗ | return 0; | ||
40 | } | |||
41 | ||||
42 | 309 | static void loadLibraries(LuaHandle& ls) { | ||
43 | 309 | constexpr luaL_Reg libs[] = { | ||
44 | // TODO: do we even need the base lib? | |||
45 | //{ LUA_GNAME, luaopen_base }, | |||
46 | { LUA_MATHLIBNAME, luaopen_math }, | |||
47 | }; | |||
48 | ||||
49 |
2/2✓ Branch 1 taken 309 times.
✓ Branch 2 taken 309 times.
|
2/2✓ Decision 'true' taken 309 times.
✓ Decision 'false' taken 309 times.
|
618 | for (size_t i = 0; i < efi::size(libs); i++) { |
50 |
1/1✓ Branch 4 taken 309 times.
|
309 | luaL_requiref(ls, libs[i].name, libs[i].func, 1); | |
51 |
1/1✓ Branch 2 taken 309 times.
|
309 | lua_pop(ls, 1); | |
52 | } | |||
53 | 309 | } | ||
54 | ||||
55 | 309 | static LuaHandle setupLuaState(lua_Alloc alloc) { | ||
56 |
1/1✓ Branch 2 taken 309 times.
|
309 | LuaHandle ls = lua_newstate(alloc, NULL); | |
57 | ||||
58 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 309 times.
|
1/2✗ Decision 'true' not taken.
✓ Decision 'false' taken 309 times.
|
309 | if (!ls) { |
59 | ✗ | criticalError("Failed to start Lua interpreter"); | ||
60 | ||||
61 | ✗ | return nullptr; | ||
62 | } | |||
63 | ||||
64 |
1/1✓ Branch 4 taken 309 times.
|
309 | lua_atpanic(ls, [](lua_State* l) { | |
65 | ✗ | criticalError("Lua panic: %s", lua_tostring(l, -1)); | ||
66 | ||||
67 | // hang the lua thread | |||
68 | ✗ | while (true) ; | ||
69 | ||||
70 | return 0; | |||
71 | }); | |||
72 | ||||
73 | // Load Lua's own libraries | |||
74 |
1/1✓ Branch 1 taken 309 times.
|
309 | loadLibraries(ls); | |
75 | ||||
76 | // Load rusEFI hooks | |||
77 |
2/2✓ Branch 2 taken 309 times.
✓ Branch 6 taken 309 times.
|
309 | lua_register(ls, "setTickRate", lua_setTickRate); | |
78 |
1/1✓ Branch 2 taken 309 times.
|
309 | configureRusefiLuaHooks(ls); | |
79 | ||||
80 | // run a GC cycle | |||
81 |
1/1✓ Branch 2 taken 309 times.
|
309 | lua_gc(ls, LUA_GCCOLLECT, 0); | |
82 | ||||
83 | // set GC settings | |||
84 | // see https://www.lua.org/manual/5.4/manual.html#2.5.1 | |||
85 |
1/1✓ Branch 2 taken 309 times.
|
309 | lua_gc(ls, LUA_GCINC, 50, 1000, 9); | |
86 | ||||
87 | 309 | return ls; | ||
88 | 309 | } | ||
89 | ||||
90 | 309 | static bool loadScript(LuaHandle& ls, const char* scriptStr) { | ||
91 | 309 | efiPrintf(TAG "loading script length: %u...", std::strlen(scriptStr)); | ||
92 | ||||
93 |
6/6✓ Branch 2 taken 308 times.
✓ Branch 3 taken 1 time.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 302 times.
✓ Branch 8 taken 7 times.
✓ Branch 9 taken 302 times.
|
2/2✓ Decision 'true' taken 7 times.
✓ Decision 'false' taken 302 times.
|
309 | if (0 != luaL_dostring(ls, scriptStr)) { |
94 | 7 | withErrorLoading = true; | ||
95 | 7 | efiPrintf(TAG "ERROR loading script: %s", lua_tostring(ls, -1)); | ||
96 | 7 | lua_pop(ls, 1); | ||
97 | 7 | return false; | ||
98 | } | |||
99 | ||||
100 | 302 | efiPrintf(TAG "script loaded successfully!"); | ||
101 | ||||
102 | #if EFI_PROD_CODE | |||
103 | luaHeapPrintInfo(); | |||
104 | #endif // EFI_PROD_CODE | |||
105 | ||||
106 | 302 | return true; | ||
107 | } | |||
108 | ||||
109 | #if !EFI_UNIT_TEST | |||
110 | static bool interactivePending = false; | |||
111 | static char interactiveCmd[100]; | |||
112 | ||||
113 | static void doInteractive(LuaHandle& ls) { | |||
114 | if (!interactivePending) { | |||
115 | // no cmd pending, return | |||
116 | return; | |||
117 | } | |||
118 | ||||
119 | auto status = luaL_dostring(ls, interactiveCmd); | |||
120 | ||||
121 | if (0 == status) { | |||
122 | // Function call was OK, resolve return value and print it | |||
123 | if (lua_isinteger(ls, -1)) { | |||
124 | efiPrintf(TAG "interactive returned integer: %d", lua_tointeger(ls, -1)); | |||
125 | } else if (lua_isnumber(ls, -1)) { | |||
126 | efiPrintf(TAG "interactive returned number: %f", lua_tonumber(ls, -1)); | |||
127 | } else if (lua_isstring(ls, -1)) { | |||
128 | efiPrintf(TAG "interactive returned string: '%s'", lua_tostring(ls, -1)); | |||
129 | } else if (lua_isboolean(ls, -1)) { | |||
130 | efiPrintf(TAG "interactive returned bool: %s", lua_toboolean(ls, -1) ? "true" : "false"); | |||
131 | } else if (lua_isnil(ls, -1)) { | |||
132 | efiPrintf(TAG "interactive returned nil."); | |||
133 | } else { | |||
134 | efiPrintf(TAG "interactive returned nothing."); | |||
135 | } | |||
136 | } else { | |||
137 | // error with interactive command, print it | |||
138 | efiPrintf(TAG "interactive error: %s", lua_tostring(ls, -1)); | |||
139 | } | |||
140 | ||||
141 | interactivePending = false; | |||
142 | ||||
143 | lua_settop(ls, 0); | |||
144 | } | |||
145 | ||||
146 | static uint32_t maxLuaDuration{}; | |||
147 | ||||
148 | static void invokeTick(LuaHandle& ls) { | |||
149 | ScopePerf perf(PE::LuaTickFunction); | |||
150 | ||||
151 | // run the tick function | |||
152 | lua_getglobal(ls, "onTick"); | |||
153 | if (lua_isnil(ls, -1)) { | |||
154 | // TODO: handle missing tick function | |||
155 | lua_settop(ls, 0); | |||
156 | return; | |||
157 | } | |||
158 | #if EFI_PROD_CODE | |||
159 | uint32_t before = port_rt_get_counter_value(); | |||
160 | #endif // EFI_PROD_CODE | |||
161 | ||||
162 | int status = lua_pcall(ls, 0, 0, 0); | |||
163 | ||||
164 | #if EFI_PROD_CODE | |||
165 | uint32_t duration = port_rt_get_counter_value() - before; | |||
166 | maxLuaDuration = std::max(maxLuaDuration, duration); | |||
167 | #endif // EFI_PROD_CODE | |||
168 | ||||
169 | if (0 != status) { | |||
170 | // error calling hook function | |||
171 | auto errMsg = lua_tostring(ls, -1); | |||
172 | efiPrintf(TAG "error %s", errMsg); | |||
173 | lua_pop(ls, 1); | |||
174 | } | |||
175 | ||||
176 | lua_settop(ls, 0); | |||
177 | } | |||
178 | ||||
179 | struct LuaThread : ThreadController<4096> { | |||
180 | LuaThread() : ThreadController("lua", PRIO_LUA) { } | |||
181 | ||||
182 | void ThreadTask() override; | |||
183 | }; | |||
184 | ||||
185 | static void resetLua() { | |||
186 | engine->module<AcController>().unmock().isDisabledByLua = false; | |||
187 | #if EFI_CAN_SUPPORT | |||
188 | resetLuaCanRx(); | |||
189 | #endif // EFI_CAN_SUPPORT | |||
190 | ||||
191 | // De-init pins, they will reinit next start of the script. | |||
192 | luaDeInitPins(); | |||
193 | } | |||
194 | ||||
195 | ||||
196 | static bool needsReset = false; | |||
197 | ||||
198 | // Each invocation of runOneLua will: | |||
199 | // - create a new Lua instance | |||
200 | // - read the script from config | |||
201 | // - run the tick function until needsReset is set | |||
202 | // Returns true if it should be re-called immediately, | |||
203 | // or false if there was a problem setting up the interpreter | |||
204 | // or parsing the script. | |||
205 | static bool runOneLua(lua_Alloc alloc, const char* script) { | |||
206 | needsReset = false; | |||
207 | ||||
208 | auto ls = setupLuaState(alloc); | |||
209 | ||||
210 | // couldn't start Lua interpreter, bail out | |||
211 | if (!ls) { | |||
212 | return false; | |||
213 | } | |||
214 | ||||
215 | // Reset default tick rate | |||
216 | luaTickPeriodUs = MS2US(5); | |||
217 | ||||
218 | if (!loadScript(ls, script)) { | |||
219 | return false; | |||
220 | } | |||
221 | ||||
222 | while (!needsReset && !chThdShouldTerminateX()) { | |||
223 | efitick_t beforeNt = getTimeNowNt(); | |||
224 | #if EFI_CAN_SUPPORT | |||
225 | // First, process any pending can RX messages | |||
226 | totalRxCount += recentRxCount; | |||
227 | recentRxCount = doLuaCanRx(ls); | |||
228 | rxTime = getTimeNowNt() - beforeNt; | |||
229 | #endif // EFI_CAN_SUPPORT | |||
230 | ||||
231 | // Next, check if there is a pending interactive command entered by the user | |||
232 | doInteractive(ls); | |||
233 | ||||
234 | invokeTick(ls); | |||
235 | ||||
236 | engine->outputChannels.luaLastCycleDuration = (getTimeNowNt() - beforeNt); | |||
237 | engine->outputChannels.luaInvocationCounter++; | |||
238 | chThdSleep(TIME_US2I(luaTickPeriodUs)); | |||
239 | ||||
240 | engine->engineState.luaDigitalState0 = getAuxDigital(0); | |||
241 | engine->engineState.luaDigitalState1 = getAuxDigital(1); | |||
242 | engine->engineState.luaDigitalState2 = getAuxDigital(2); | |||
243 | engine->engineState.luaDigitalState3 = getAuxDigital(3); | |||
244 | } | |||
245 | ||||
246 | resetLua(); | |||
247 | ||||
248 | return true; | |||
249 | } | |||
250 | ||||
251 | void LuaThread::ThreadTask() { | |||
252 | while (!chThdShouldTerminateX()) { | |||
253 | bool wasOk = runOneLua(luaHeapAlloc, config->luaScript); | |||
254 | ||||
255 | auto usedAfterRun = luaHeapUsed(); | |||
256 | if (usedAfterRun != 0) { | |||
257 | if (!withErrorLoading) { | |||
258 | efiPrintf(TAG "MEMORY LEAK DETECTED: %d bytes used after teardown", usedAfterRun); | |||
259 | } | |||
260 | ||||
261 | // Lua blew up in some terrible way that left memory allocated, reset the heap | |||
262 | // so that subsequent runs don't overflow the heap | |||
263 | luaHeapReset(); | |||
264 | } | |||
265 | ||||
266 | // Reset any lua adjustments the script made | |||
267 | engine->resetLua(); | |||
268 | ||||
269 | if (!wasOk) { | |||
270 | // Something went wrong executing the script, spin | |||
271 | // until reset invoked (maybe the user fixed the script) | |||
272 | while (!needsReset) { | |||
273 | chThdSleepMilliseconds(100); | |||
274 | } | |||
275 | } | |||
276 | } | |||
277 | } | |||
278 | ||||
279 | static LuaThread luaThread; | |||
280 | ||||
281 | void startLua() { | |||
282 | luaHeapInit(); | |||
283 | ||||
284 | #if EFI_CAN_SUPPORT | |||
285 | initLuaCanRx(); | |||
286 | #endif // EFI_CAN_SUPPORT | |||
287 | ||||
288 | addConsoleActionII("set_lua_setting", [](int index, int value) { | |||
289 | engineConfiguration->scriptSetting[index] = value; | |||
290 | }); | |||
291 | ||||
292 | luaThread.start(); | |||
293 | ||||
294 | addConsoleActionS("lua", [](const char* str){ | |||
295 | if (interactivePending) { | |||
296 | return; | |||
297 | } | |||
298 | ||||
299 | strncpy(interactiveCmd, str, sizeof(interactiveCmd) - 1); | |||
300 | interactiveCmd[sizeof(interactiveCmd) - 1] = '\0'; | |||
301 | ||||
302 | interactivePending = true; | |||
303 | }); | |||
304 | ||||
305 | addConsoleAction("luareset", [](){ | |||
306 | needsReset = true; | |||
307 | }); | |||
308 | ||||
309 | addConsoleAction("luamemory", [](){ | |||
310 | efiPrintf("maxLuaDuration %lu", maxLuaDuration); | |||
311 | maxLuaDuration = 0; | |||
312 | efiPrintf("rx total/recent/dropped %d %d %d", totalRxCount, | |||
313 | recentRxCount, getLuaCanRxDropped()); | |||
314 | efiPrintf("luaCycle %luus including luaRxTime %dus", NT2US(engine->outputChannels.luaLastCycleDuration), | |||
315 | NT2US(rxTime)); | |||
316 | ||||
317 | luaHeapPrintInfo(); | |||
318 | }); | |||
319 | } | |||
320 | ||||
321 | #else // not EFI_UNIT_TEST | |||
322 | ||||
323 | 1 | void startLua() { } | ||
324 | ||||
325 | #include <stdexcept> | |||
326 | #include <string> | |||
327 | ||||
328 | 100 | static LuaHandle runScript(const char* script) { | ||
329 | 100 | auto ls = setupLuaState(luaHeapAlloc); | ||
330 | ||||
331 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 100 times.
|
1/2✗ Decision 'true' not taken.
✓ Decision 'false' taken 100 times.
|
100 | if (!ls) { |
332 | ✗ | throw std::logic_error("Call to setupLuaState failed, returned null"); | ||
333 | } | |||
334 | ||||
335 |
3/3✓ Branch 1 taken 100 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 99 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 99 times.
|
100 | if (!loadScript(ls, script)) { |
336 |
1/1✓ Branch 2 taken 1 time.
|
1 | throw std::logic_error("Call to loadScript failed"); | |
337 | } | |||
338 | ||||
339 |
1/1✓ Branch 2 taken 99 times.
|
99 | lua_getglobal(ls, "testFunc"); | |
340 |
3/3✓ Branch 2 taken 99 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 98 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 98 times.
|
99 | if (lua_isnil(ls, -1)) { |
341 |
1/1✓ Branch 2 taken 1 time.
|
1 | throw std::logic_error("Failed to find function testFunc"); | |
342 | } | |||
343 | ||||
344 |
1/1✓ Branch 2 taken 98 times.
|
98 | int status = lua_pcall(ls, 0, 1, 0); | |
345 | ||||
346 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 98 times.
|
1/2✗ Decision 'true' not taken.
✓ Decision 'false' taken 98 times.
|
98 | if (0 != status) { |
347 | ✗ | std::string msg = std::string("lua error while running script: ") + lua_tostring(ls, -1); | ||
348 | ✗ | throw std::logic_error(msg); | ||
349 | ✗ | } | ||
350 | ||||
351 | 98 | return ls; | ||
352 | 2 | } | ||
353 | ||||
354 | 83 | expected<float> testLuaReturnsNumberOrNil(const char* script) { | ||
355 |
1/1✓ Branch 2 taken 83 times.
|
83 | auto ls = runScript(script); | |
356 | ||||
357 | // check nil return first | |||
358 |
3/3✓ Branch 2 taken 83 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 78 times.
|
2/2✓ Decision 'true' taken 5 times.
✓ Decision 'false' taken 78 times.
|
83 | if (lua_isnil(ls, -1)) { |
359 | 5 | return unexpected; | ||
360 | } | |||
361 | ||||
362 | // If not nil, it should be a number | |||
363 |
2/3✓ Branch 2 taken 78 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 78 times.
|
1/2✗ Decision 'true' not taken.
✓ Decision 'false' taken 78 times.
|
78 | if (!lua_isnumber(ls, -1)) { |
364 | ✗ | throw std::logic_error("Returned value is not a number"); | ||
365 | } | |||
366 | ||||
367 | // pop the return value | |||
368 |
1/1✓ Branch 3 taken 78 times.
|
78 | return lua_tonumber(ls, -1); | |
369 | 83 | } | ||
370 | ||||
371 | 11 | float testLuaReturnsNumber(const char* script) { | ||
372 |
1/1✓ Branch 2 taken 11 times.
|
11 | auto ls = runScript(script); | |
373 | ||||
374 | // check the return value | |||
375 |
2/3✓ Branch 2 taken 11 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 11 times.
|
1/2✗ Decision 'true' not taken.
✓ Decision 'false' taken 11 times.
|
11 | if (!lua_isnumber(ls, -1)) { |
376 | ✗ | throw new std::logic_error("Returned value is not a number"); | ||
377 | } | |||
378 | ||||
379 | // pop the return value | |||
380 |
1/1✓ Branch 2 taken 11 times.
|
22 | return lua_tonumber(ls, -1); | |
381 | 11 | } | ||
382 | ||||
383 | 6 | int testLuaReturnsInteger(const char* script) { | ||
384 |
1/1✓ Branch 2 taken 4 times.
|
6 | auto ls = runScript(script); | |
385 | ||||
386 | // pop the return value; | |||
387 |
3/3✓ Branch 2 taken 4 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 2 times.
|
2/2✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 2 times.
|
4 | if (!lua_isinteger(ls, -1)) { |
388 |
1/1✓ Branch 2 taken 2 times.
|
2 | throw std::logic_error("Returned value is not an integer"); | |
389 | } | |||
390 | ||||
391 |
1/1✓ Branch 2 taken 2 times.
|
4 | return lua_tointeger(ls, -1); | |
392 | 4 | } | ||
393 | ||||
394 | 209 | void testLuaExecString(const char* script) { | ||
395 |
1/1✓ Branch 2 taken 209 times.
|
209 | auto ls = setupLuaState(luaHeapAlloc); | |
396 | ||||
397 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 209 times.
|
1/2✗ Decision 'true' not taken.
✓ Decision 'false' taken 209 times.
|
209 | if (!ls) { |
398 | ✗ | throw std::logic_error("Call to setupLuaState failed, returned null"); | ||
399 | } | |||
400 | ||||
401 |
3/3✓ Branch 1 taken 209 times.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 203 times.
|
2/2✓ Decision 'true' taken 6 times.
✓ Decision 'false' taken 203 times.
|
209 | if (!loadScript(ls, script)) { |
402 |
1/1✓ Branch 2 taken 6 times.
|
6 | throw std::logic_error("Call to loadScript failed"); | |
403 | } | |||
404 | 412 | } | ||
405 | ||||
406 | #endif // EFI_UNIT_TEST | |||
407 | #endif // EFI_LUA | |||
408 | ||||
409 | // This is technically non-compliant, but it's only used for lua float parsing. | |||
410 | // It doesn't properly handle very small and very large numbers, and doesn't | |||
411 | // parse numbers in the format 1.3e5 at all. | |||
412 | 68 | extern "C" float strtof_rusefi(const char* str, char** endPtr) { | ||
413 | 68 | bool afterDecimalPoint = false; | ||
414 | 68 | float div = 1; // Divider to place digits after the decimal point | ||
415 | ||||
416 |
1/2✓ Branch 0 taken 68 times.
✗ Branch 1 not taken.
|
1/2✓ Decision 'true' taken 68 times.
✗ Decision 'false' not taken.
|
68 | if (endPtr) { |
417 | 68 | *endPtr = const_cast<char*>(str); | ||
418 | } | |||
419 | ||||
420 | 68 | float integerPart = 0; | ||
421 | 68 | float fractionalPart = 0; | ||
422 | ||||
423 |
2/2✓ Branch 0 taken 322 times.
✓ Branch 1 taken 67 times.
|
2/2✓ Decision 'true' taken 322 times.
✓ Decision 'false' taken 67 times.
|
389 | while (*str != '\0') { |
424 | 322 | char c = *str; | ||
425 | 322 | int digitVal = c - '0'; | ||
426 | ||||
427 |
4/4✓ Branch 0 taken 255 times.
✓ Branch 1 taken 67 times.
✓ Branch 2 taken 254 times.
✓ Branch 3 taken 1 time.
|
2/2✓ Decision 'true' taken 254 times.
✓ Decision 'false' taken 68 times.
|
322 | if (c >= '0' && c <= '9') { |
428 |
2/2✓ Branch 0 taken 93 times.
✓ Branch 1 taken 161 times.
|
2/2✓ Decision 'true' taken 93 times.
✓ Decision 'false' taken 161 times.
|
254 | if (!afterDecimalPoint) { |
429 | // Integer part | |||
430 | 93 | integerPart = 10 * integerPart + digitVal; | ||
431 | } else { | |||
432 | // Fractional part | |||
433 | 161 | fractionalPart = 10 * fractionalPart + digitVal; | ||
434 | 161 | div *= 10; | ||
435 | } | |||
436 |
2/2✓ Branch 0 taken 67 times.
✓ Branch 1 taken 1 time.
|
2/2✓ Decision 'true' taken 67 times.
✓ Decision 'false' taken 1 time.
|
68 | } else if (c == '.') { |
437 | 67 | afterDecimalPoint = true; | ||
438 | } else { | |||
439 | 1 | break; | ||
440 | } | |||
441 | ||||
442 | 321 | str++; | ||
443 | ||||
444 |
1/2✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
|
1/2✓ Decision 'true' taken 321 times.
✗ Decision 'false' not taken.
|
321 | if (endPtr) { |
445 | 321 | *endPtr = const_cast<char*>(str); | ||
446 | } | |||
447 | } | |||
448 | ||||
449 | 68 | return integerPart + fractionalPart / div; | ||
450 | } | |||
451 |