Method for acquiring call stack and computer equipment
By adding a back-to-stack statement to the Rust programming language runtime, the unfolding of asynchronous tasks on threads is controlled, solving the problem of difficulty in obtaining the call stack for stackless coroutines. This achieves efficient and unaffected call stack retrieval and provides accurate coroutine execution judgment.
Patent Information
- Application Number
- CN202411441462.7
- Authority / Receiving Office
- CN · China
- Patent Type
- Applications(China)
- Current Assignee / Owner
- Filing Date
- 2024-10-15
- Publication Date
- 2026-04-17
AI Technical Summary
In the Rust programming language, it is difficult to obtain the call stack of a stackless coroutine, making it difficult to accurately determine the coroutine's running status.
By adding a stack push statement to the Rust programming language runtime, asynchronous tasks can be controlled to unfold on threads. When the call stack is obtained, the task logic is not executed. The state information of the asynchronous task is converted into a call stack and controlled at runtime.
It enables efficient and unaffected retrieval of the call stack in the Rust programming language, avoiding interference with task logic and runtime overhead, and providing accurate coroutine execution judgment.
Smart Images

Figure CN121880123A_ABST
Abstract
Description
Technical Field
[0001] This application relates to the field of electronic devices, and more specifically, to a method for obtaining a call stack and a computer device. Background Technology
[0002] Rust, as a systems programming language, provides a coroutine asynchronous capability (Future), which allows programs to run or switch asynchronously. During program execution, the call stack is often accessed to capture and record function call relationships at the current or past point in time. However, accessing the call stack is very difficult in Rust, making it challenging to determine the status of coroutines.
[0003] Therefore, how to provide a convenient and efficient method to obtain the call stack so as to accurately determine the running status of coroutines in the Rust programming language is an urgent problem to be solved. Summary of the Invention
[0004] This application provides a method and computer device for obtaining the call stack. This method allows for convenient and efficient acquisition of the call stack, enabling accurate assessment of the execution status of coroutines in the Rust programming language.
[0005] In a first aspect, a method for obtaining a call stack is provided, the method comprising: obtaining a source code file including a first statement indicating that the call stack should be obtained; and running the source code file by a runtime including a second statement to obtain the call stack, the second statement being used to determine whether the task of the current run is to obtain the call stack.
[0006] Based on the above solution, a stack push statement is added at runtime to increase runtime control and unfold asynchronous tasks on threads. When the task is to retrieve the call stack, the asynchronous task can be pushed back onto the stack, thereby obtaining and recording the call stack. This process does not require extensive modifications to the code files, making call stack retrieval convenient and efficient.
[0007] In conjunction with the first aspect, in some implementations of the first aspect, the source code file includes a first code block, the first code block includes a first sub-code block, the execution result of the first sub-code block is used to determine the execution result of the first code block, and the first code block includes the first statement.
[0008] Generally, a code file includes at least one code block, and each code block contains at least one code statement. The first sub-code block mentioned above can also be understood as a code statement. Since the execution result of the first sub-code block is used to determine the execution result of the first sub-code block, it can be seen that the source code file involves asynchronous functions, or the source code file is used to implement asynchronous tasks.
[0009] It's important to note that, according to the execution principle of asynchronous tasks in the Rust programming language, during the execution of each task, if the task is completed and ready to produce a result, the poll function will return Poll::Ready and provide the result. The task is then complete, and the next task can proceed. If a task encounters blocking behavior (e.g., unable to acquire a lock, network congestion, etc.), the poll function will return Poll::Pending. This execution will exit, but the task that encountered blocking behavior during this execution will be retained. When this task is scheduled again, the state machine will still maintain the state corresponding to that task. Therefore, in the next execution, one can enter the poll function from this state and continue executing the task corresponding to that state.
[0010] For example, if a blocking event occurred during the previous execution of the task in the first sub-block, the previous execution would have exited at the location of the first sub-block. In the next execution, it will continue from the first sub-block, enter the poll function, and continue executing the task corresponding to the first sub-block.
[0011] It is evident that the execution of the first sub-code block was blocked. The next execution can continue from the first sub-code block. If the task of the next execution is to obtain the call stack, then the call stack of the first sub-code block that was blocked in the previous execution can be obtained, that is, the function call relationship corresponding to the first sub-code block can be obtained, so as to make an accurate judgment on the running status of the coroutine.
[0012] It's easy to understand that the call stack of an asynchronous task typically goes through multiple await nodes. The node at the very end, or the one that might actually cause blocking behavior, is the leaf node. The first sub-code block mentioned above can be understood as a leaf node in the data structure.
[0013] In conjunction with the first aspect, in some implementations of the first aspect, the runtime also includes a third statement for executing the functionality of the first sub-code block, with the second statement preceding the third statement.
[0014] It's important to note that to avoid impacting task logic when retrieving the call stack, a statement for pushing back the stack can be added before the statement used to run the task logic. This second statement for pushing back the stack precedes the third statement used to run the code block. If the task being run is to retrieve the call stack, the push-back statement will be executed first, the call stack will be recorded, and the execution will exit. Clearly, this run does not execute the functionality of the first sub-code block corresponding to the third statement, thus not changing the task data and state of the stackless coroutine.
[0015] In conjunction with the first aspect, in some implementations of the first aspect, the first statement includes a first handle, which is specifically used to indicate the call stack of the first sub-code block by means of the first handle; and / or the first statement includes a first name, which is specifically used to indicate the call stack of the first sub-code block by means of the first name.
[0016] In conjunction with the first aspect, in some implementations of the first aspect, the first code block further includes a fourth statement, which is used to define the first handle of the first sub-code block, and / or the fourth statement is used to define the first name of the first sub-code block.
[0017] It should be noted that users can define the name and / or handle of the asynchronous task in the source code file, and determine that the purpose of this run is to obtain the call stack by executing the handle or by specifying the name.
[0018] In conjunction with the first aspect, in some implementations of the first aspect, the function of the first sub-code is one of the following: semaphore, lock, network, sleep.
[0019] In conjunction with the first aspect, in some implementations of the first aspect, the source code file is a code file in the Rust programming language.
[0020] In a second aspect, an apparatus for obtaining a call stack is provided. The apparatus includes a processing module for obtaining a source code file, the source code file including a first statement indicating that the call stack should be obtained; and for running the source code file through a runtime including a second statement to obtain the call stack, the second statement being used to determine whether the task of the current run is to obtain the call stack.
[0021] In conjunction with the second aspect, in some implementations of the second aspect, the source code file includes a first code block, the first code block includes a first sub-code block, the execution result of the first sub-code block is used to determine the execution result of the first code block, and the first code block includes the first statement.
[0022] In conjunction with the second aspect, in some implementations of the second aspect, the runtime also includes a third statement that executes the functionality of the first sub-code block, with the second statement preceding the third statement.
[0023] In conjunction with the second aspect, in some implementations of the second aspect, the first statement includes a first handle, which is specifically used to indicate the call stack of the first sub-code block by means of the first handle; and / or the first statement includes a first name, which is specifically used to indicate the call stack of the first sub-code block by means of the first name.
[0024] In conjunction with the second aspect, in some implementations of the second aspect, the first code block further includes a fourth statement, which is used to define the first handle of the first sub-code block, and / or the fourth statement is used to define the first name of the first sub-code block.
[0025] In conjunction with the second aspect, in some implementations of the second aspect, the function of the first sub-code is one of the following: semaphore, lock, network, sleep.
[0026] In conjunction with the second aspect, in some implementations of the second aspect, the source code file is a code file in the Rust programming language.
[0027] Thirdly, a computer device is provided, including a processor and a memory, the memory being used to store program instructions, and the processor being used to invoke the program instructions to perform a method as described in the first aspect and any possible implementation thereof.
[0028] Fourthly, a computer-readable storage medium is provided having a computer program stored thereon, which, when executed by a computer, enables the implementation of the methods described in the first aspect and any possible implementation thereof.
[0029] Fifthly, a computer program product is provided, comprising a computer program that, when executed by a processor, implements the methods described in the first aspect and any possible implementation thereof.
[0030] In a sixth aspect, a chip is provided, including a processor for reading instructions stored in a memory, wherein when the processor executes the instructions, the chip implements the methods of the first aspect and any possible implementation thereof. Attached Figure Description
[0031] Figure 1 This is a schematic diagram of obtaining the call stack provided in an embodiment of this application.
[0032] Figure 2 This is a schematic diagram of a scenario provided in an embodiment of this application.
[0033] Figure 3 This is a schematic diagram illustrating the execution principle of an asynchronous task in the Rust programming language, as provided in an embodiment of this application.
[0034] Figure 4 This is a schematic flowchart illustrating a method for obtaining the call stack provided in an embodiment of this application.
[0035] Figure 5 This is a schematic diagram of the structure of a call stack acquisition device provided in an embodiment of this application.
[0036] Figure 6This is a schematic diagram of a computer device structure provided in an embodiment of this application. Detailed Implementation
[0037] The technical solutions in this application will now be described with reference to the accompanying drawings.
[0038] The terminology used in the following embodiments is for the purpose of describing particular embodiments only and is not intended to be limiting of this application. As used in the specification and appended claims of this application, the singular expressions “a,” “an,” “the,” “the,” “the,” and “this” are intended to also include expressions such as “one or more,” unless the context clearly indicates otherwise. It should also be understood that in the following embodiments of this application, “at least one” and “one or more” refer to one, two, or more than two. The term “and / or” is used to describe the relationship between related objects, indicating that three relationships may exist; for example, A and / or B can indicate: A alone, A and B simultaneously, or B alone, where A and B can be singular or plural. The character “ / ” generally indicates that the preceding and following related objects are in an “or” relationship.
[0039] References to "one embodiment" or "some embodiments" as described in this specification mean that one or more embodiments of this application include a specific feature, structure, or characteristic described in connection with that embodiment. Therefore, the phrases "in one embodiment," "in some embodiments," "in other embodiments," "in still other embodiments," etc., appearing in different parts of this specification do not necessarily refer to the same embodiment, but rather mean "one or more, but not all, embodiments," unless otherwise specifically emphasized. The terms "comprising," "including," "having," and variations thereof mean "including but not limited to," unless otherwise specifically emphasized.
[0040] The Rust programming language provides a coroutine asynchronous capability (Future), which allows programs to run or switch asynchronously.
[0041] In the Rust programming language, both thread and coroutine contexts exist simultaneously. Stackless coroutines are an important concurrency feature of Rust. While function call relationships can usually be determined by obtaining the call stack (backtrace), obtaining the call stack for Rust stackless coroutines is very difficult, making it hard to accurately determine the coroutine's execution status.
[0042] Rust's stackless coroutines are primarily implemented using asynchronous functions `async fn` and the `await` keyword. This stackless coroutine implementation does not rely on the system thread's stack space; instead, it manages coroutine execution by generating a state machine. When an `async fn` function is called, it doesn't execute immediately but returns a type that implements the `Future` trait. This `Future` object represents a future result that can be completed at some point in the future. The `await` keyword is used to wait for a `Future` to complete, pausing the execution of the current asynchronous function until the `Future` is ready. This mechanism allows Rust to perform asynchronous operations without blocking, improving program concurrency performance. In Rust's asynchronous programming, the core of the `Future` trait is the `poll` function. The `poll` function is periodically called by the executor to check if the `Future` is ready to produce a result. If the `Future` is not ready, the `poll` function returns `Poll::Pending`, and the executor will try again in the future. If the `Future` is ready, the `poll` function returns `Poll::Ready` and provides the result.
[0043] With the widespread adoption of the Rust programming language, the need to obtain the call stack has become increasingly prominent. Currently, there are two main methods for obtaining the call stack.
[0044] Method 1: Obtain the call stack by utilizing the asynchronous function call stack recording capability provided by await-tree.
[0045] For example, the source code file is as follows:
[0046]
[0047] The source code file above exemplifies the call process of three asynchronous functions. To enable the recording of asynchronous function call stacks, an await-tree can be introduced into the source code file.
[0048] For example, the code file obtained after including await-tree in the source code file is as follows:
[0049] 1usage
[0050] async fn bar(i:i32){
[0051] baz(i).instrument_await("baz in bar").await
[0052] }
[0053] 2usages
[0054] async fn bar(i:i32){
[0055] pending()
[0056] .instrument_await(format!("pending in baz{i}"))
[0057] .await
[0058] }
[0059] 2usages
[0060] async fn foo(){
[0061] join(
[0062] baz(i:3).instrument_await("baz"),
[0063] baz(i:2).instrument_await("baz"),
[0064] ).await;
[0065] }
[0066] Introducing await-tree into the source code file and manually adding `instrument_await` to each await call node, as shown in bold in the code file above, further complicates matters. This requires manual string editing, necessitating users to modify the code of each coroutine, resulting in extensive modifications. This manually edited string allocates extra space during function execution; each time the function is called, the string needs to remain in memory, introducing additional runtime overhead. This overhead persists even if the user doesn't retrieve the call stack. The significant runtime overhead of retrieving the call stack will affect the normal function call.
[0067] Method 2: Obtain the call stack by observing the behavior of the function call stack on the thread.
[0068] Traditional code programming is mostly thread-based, and each function has its own stack space, which can also be called a stack frame. A stack frame is created when a function is called and destroyed when the function returns.
[0069] For example, such as Figure 1The diagram illustrates a method for obtaining the function call stack on a thread. When the main function calls the func1 function, the stack grows from top to bottom, pushing the values of the PC, LP, SP, and FP registers, along with the function's parameters, onto the stack in sequence as historical records. SP points to the current stack frame, and FP points to the function stack frame of the previous frame.
[0070] For example, when the code is inside the `func1` function, the stored `SP` and `FP` can be retrieved on the `func1` function's stack. `SP` points to the current stack frame, and `FP` points to the parent stack frame. Therefore, we can deduce that the parent function of `func1` is the `main` function. The same process can be repeated within the `main` function to determine its parent function, and so on, thus establishing the function call relationships.
[0071] This method is based on the principle of threads, where each thread has its own independent stack space, thus allowing access to the function call stack on the thread. However, in the Rust programming language, stackless coroutines do not have independent stack space, so method two is clearly not suitable for obtaining the call stack of stackless coroutines in Rust.
[0072] It is evident that the current method of obtaining the call stack is not suitable for stackless coroutines in the Rust programming language. Therefore, this application provides a method and computer device for obtaining the call stack, which can convert the State information in a stackless coroutine into a call stack and control it at runtime. This allows the task logic to be omitted when performing the task of obtaining the call stack on a stackless coroutine, thus obtaining the call stack while preventing any impact on the task.
[0073] like Figure 2 The diagram illustrates a scenario provided by an embodiment of this application. Figure 2 The runtime in Rust is the necessary environment for asynchronous functions to run, primarily responsible for scheduling and executing asynchronous tasks. Figure 2 In the example, the runtime includes 6 asynchronous tasks (i.e., coroutines) and 3 worker threads. Asynchronous tasks 4, 5, and 6 are currently executing on worker threads, while asynchronous tasks 1, 2, and 3 are in a pending state, waiting to be scheduled.
[0074] Users can define and create asynchronous tasks, specifying their names and handles. Asynchronous tasks can be scheduled to begin running at runtime. After a task has been running for a period of time, users can retrieve the corresponding call stack from the runtime by specifying the name or handle.
[0075] For example, the above process can be represented by the following code file:
[0076] 1. async fn funcA(){
[0077] 2. / / This is the definition of an asynchronous task.
[0078] 3.} 4.
[0080] 5.fn main(){
[0081] 6. / / Create an asynchronous task
[0082] 7.let handle=Runtime::spawn("TaskA",funcA());
[0083] 8. / / After a period of time
[0084] 9. sleep(10);
[0085] 10. / / Get the call stack by specifying a handle
[0086] 11.let backtrace=Runtime::get_backtrace(handle);
[0087] 12. / / Get the call stack by a specified name
[0088] 13.let backtrace=Runtime::get_backtrace("TaskA");
[0089] 14. / / View the call stack
[0090] 15. print(backtrace);
[0091] 16.}
[0092] It's important to note that the `async` keyword is used to declare the function `funcA` as an asynchronous function. This means that when `funcA` is called, it will not execute immediately but will return a `Future` trait. This `Future` represents the result of the asynchronous operation, which can be completed at some point in the future. An asynchronous task named "TaskA" is created, which involves the function `funcA`, and a handle is defined for this task. This task is part of the asynchronous runtime, allowing the asynchronous task to be submitted to the runtime for concurrent execution. After the task has run for a period of time, if the user wants to know the call stack of the task, they can obtain the corresponding call stack from the runtime by executing the name "TaskA" or by using the handle.
[0093] For example, a regular call stack can look like this:
[0094] Backtrace is:
[0095] 0:<runtime::sync::semaphore_inner::Permit as core::future::future::Future> :;poll
[0096] at. / src / sync / semaphore_inner.rs:265:47
[0097] 1:runtime::sync::mutex::Mutex <t>::lock::{{closure}}
[0098] at. / src / sync / mutex.rs:106:28
[0099] 2:trace::misson1::{{closure}}
[0100] at. / tests / trace.rs:469:31
[0101] 3:trace::func::{{closure}}
[0102] at. / tests / trace.rs:482:32
[0103] 4:trace::test_func::{{closure}}
[0104] at. / tests / trace.rs:500:28
[0105] 5:runtime::task::raw::Inner<T,S>::raw_poll
[0106] at. / src / task / raw.rs:295:20
[0107] 6:runtime::task::task_handle::TaskHandle<T,S>::raw_poll
[0108] at. / src / task / task_handle.rs:237:9
[0109] 7:runtime::task::raw::raw_poll
[0110] at. / src / task / raw.rs:379:8
[0111] 8:runtime::task::raw::RawTask::raw_poll
[0112] at. / src / task / raw.rs:148:18
[0113] Specifically, the following shows a source code file:
[0114] async fn func(){
[0115] mission1().await;
[0116] mission2().await;
[0117] mission3().await;
[0118] }
[0119] In the Rust programming language, for each Future function, the compiler generates a struct named Generator and a Poll function. The Poll function is used to drive the execution of asynchronous tasks and is usually used in conjunction with a Future function. The Future represents the result of the asynchronous operation, and the Poll function checks whether the result is available.
[0120] For example, the code file generated by the compiler is shown below:
[0121]
[0122] As can be seen, Future functions actually exist as structs and methods. The Generator memory layout for each Future function is different and belongs to the compiler's internal information, invisible to the user. That is, the code file generated by the compiler is invisible; therefore, stackless coroutines do not have independent stack space.
[0123] It's important to note that the asynchronous function can be executed based on the state and data information in the Generator, as well as the actual poll function. The state information can have multiple values, such as Init, State0, State1, and State2 in the code file generated by the compiler. Each state information corresponds to an await node. In other words, each state information corresponds to an asynchronous call.
[0124] like Figure 3 The diagram illustrates the execution principle of asynchronous tasks in the Rust programming language. During the execution of each task, if the task is completed and ready to produce a result, the `poll` function returns `Poll::Ready` and provides the result. The task is then complete, and the next task can be executed. If a task encounters blocking behavior (e.g., unable to acquire a lock, network congestion, etc.), the `poll` function returns `Poll::Pending`. This execution will exit, but the task that encountered blocking behavior during this execution will be retained. When the task is scheduled again, the state machine will still maintain the state corresponding to that task. Therefore, in the next execution, one can enter the `poll` function from that state and continue executing the task corresponding to that state. In other words, if blocking behavior is encountered during the execution of any task, the execution will exit directly, and in the next execution, it will continue from the previously blocked task. This is the principle of asynchronous task execution in the Rust programming language.
[0125] For example, from Figure 3 As can be seen, during the execution of an asynchronous task, task 1 (mission1) is executed first. If mission1 executes normally without blocking, the poll function returns Poll::Ready, and task 2 (mission2) can continue to be executed. If mission2 executes normally without blocking, the poll function returns Poll::Ready, and task 3 (mission3) can continue to be executed. If mission3 executes normally without blocking, the poll function returns Poll::Ready, and the asynchronous task execution ends. All three tasks have been executed normally and obtained results. If mission1 encounters blocking, the poll function returns Poll::Pending, exiting the current task execution, and the next execution will start from mission1. If mission1 executes normally, but mission2 encounters blocking, the poll function returns Poll::Pending, exiting the current task execution, and the next execution will start from mission2. If mission1 and mission2 execute normally, but mission3 encounters blocking behavior, the poll function returns Poll::Pending, exiting the current task execution. In the next task execution, it will start from mission3.
[0126] Based on the code files generated by the encoder, it can be seen that each state information corresponds to an asynchronous call. The `Int` state information corresponds to task initialization, indicating that the task has not yet been executed. `State0` corresponds to task 1 (mission 1), indicating that the task has been executed up to mission 1. `State1` corresponds to task 2 (mission 2), indicating that the task has been executed up to mission 2. `State2` corresponds to task 3 (mission 3), indicating that the task has been executed up to mission 3.
[0127] For example, if a blocking event occurs during the execution of task 3 (mission 3) corresponding to state 2, the execution will return Pending and exit, retaining state 2. When the task is scheduled again, since the state machine is still in state 2, it will enter the poll function with state 2 and continue executing task 3 (mission 3).
[0128] As can be seen from the above explanation of the asynchronous task operation principle in the Rust programming language, stackless coroutines in Rust may exit at any await node. When an execution exits from any await node, the next execution will enter the same await node. State is used to record the coroutine's state information, but State is an integer, meaning that the code file generated by the compiler contains internal compiler information and is not visible to the user.
[0129] Based on this, the method and computer device for obtaining the call stack provided in this application embodiment can convert State information into a call stack and control it during runtime, so that the execution of obtaining the call stack will not execute task logic and will not affect the task while obtaining the call stack.
[0130] For example, the source code file created by the user is as follows:
[0131]
[0132] It's easy to see that there's a certain calling relationship between the functions f1, f2, f3, and f4 in the source code file. The call stack of an asynchronous task typically traverses multiple await nodes. The node at the very end, or the one that might actually cause blocking behavior (the poll function will return Pending), is the leaf node; the remaining nodes are intermediate nodes. Leaf nodes contain functions provided for runtime (e.g., locks, semaphores, network message sending and receiving).
[0133] Before using the method for obtaining the call stack provided in the embodiments of this application, the runtime code file is as follows:
[0134]
[0135] The code file above shows an `impl` block in Rust that implements the `Future` trait for the function `f4`. `f4` is an asynchronous task whose execution can be advanced using the `poll` method. It defines the associated type of the `Future` trait and defines the `poll` method, which is called by the asynchronous executor to check if the asynchronous operation has completed. If the `Future` is ready, it returns `Poll::Ready` with the result; otherwise, it returns `Poll::Pending`.
[0136] The runtime code file mentioned above needs to be modified. The modified runtime file can use the method for obtaining the call stack provided in the embodiments of this application.
[0137] For example, the modified runtime code file is as follows:
[0138]
[0139] It's easy to see that the modified code file includes the bolded content mentioned above. By adding this content to the runtime code file, control over stackless coroutines can be achieved, thereby obtaining the call stack.
[0140] As explained above regarding the execution principle of Rust's asynchronous task lines, if the previous execution exited at a leaf node, the next execution will still enter that leaf node. Therefore, the coroutine's poll function can be executed to enter the coroutine's previous exit node.
[0141] In the revised code file, the newly added bolded section contains statements for returning to the call stack. These bolded statements indicate that if the purpose of the current execution is to obtain the call stack, then the call stack is recorded and the process returns. If the purpose of the current execution is not to obtain the call stack, then the task logic executes normally. Therefore, the process of obtaining the call stack for stackless coroutines is mainly controlled by adding statements for returning to the call stack in the runtime code file.
[0142] Typically, the poll function of a coroutine is executed on a thread, for example, to execute user logic. However, if this execution is for obtaining the call stack, only the statements starting with the leaf nodes will be executed, and subsequent user statements will not be involved, thus having no impact on the task logic. This execution merely expands the abstract state information and transforms it into a concrete call stack.
[0143] This execution of the pushback statement will record the call stack and exit the current execution. Clearly, this execution will not change any data or state of the stackless coroutine.
[0144] Users can define the name and / or handle of the asynchronous task, and specify the handle or name to determine that the execution is to obtain the call stack.
[0145] It should be noted that coroutines are typically scheduled and executed on threads by the runtime. Therefore, in the method for obtaining the call stack provided in this application embodiment, at runtime, it is necessary to schedule the stackless coroutine (or asynchronous task) onto a thread. If the current task of the stackless coroutine (or asynchronous task) on the thread needs to obtain the call stack, the traditional thread stack push method can be used to push the coroutine back onto the stack, thereby obtaining the call stack. This application embodiment does not limit the specific method of stack push.
[0146] Based on the above solution, a stack push statement is added to the runtime code file of the Rust programming language to increase runtime control. Asynchronous tasks (or coroutines) are expanded on threads. When the execution is for obtaining the call stack, the task on the thread is pushed back onto the stack, thereby obtaining and recording the call stack. This execution process will not change the data and state of the stackless coroutine and will not affect the task logic. Clearly, the method for obtaining the call stack provided in this application embodiment does not require extensive modifications to the code file and does not incur significant runtime overhead to obtain the call stack, thus avoiding impact on function calls or task logic.
[0147] like Figure 4 The diagram shows a schematic flowchart of a method for obtaining a call stack provided in this application. This method for obtaining a call stack can be executed by a computer, such as a terminal device like a personal computer, laptop, smartphone, or tablet, or a standalone server or a server cluster composed of multiple servers.
[0148] S401, retrieve source code file. The source code file includes the first statement, which is used to instruct the retrieval of the call stack.
[0149] It should be noted that the source code file can be a user-created Rust programming language code file.
[0150] For example, the first statement could be `let backtrace = Runtime::get_backtrace(handle)`, which can be used to instruct the retrieval of the call stack of the asynchronous task corresponding to `handle`.
[0151] In one implementation, the source code file includes a first code block, the first code block includes a first sub-code block, the execution result of the first sub-code block is used to determine the execution result of the first code block, and the first code block includes a first statement.
[0152] For example, the first sub-block can function as one of the following: semaphore, lock, network, sleep.
[0153] When the first sub-block functions as a semaphore, it can be understood that the block uses a semaphore to control access to certain resources or coordinate the operations of multiple threads. When the first sub-block functions as a sleep block, it can be understood that the block can pause thread execution. When the first sub-block functions as a lock, it can be understood that the block uses a locking mechanism to ensure safe access to shared resources. When the first sub-block functions as a network block, it can be understood that the block involves network communication operations during execution.
[0154] For example, the first code block is as follows:
[0155]
[0156] f4.await can be understood as the first sub-code block. Code blocks are used to execute the parts of the functionality that the user wants to perform.
[0157] Generally speaking, a source code file includes at least one code block, and the code block includes at least one code statement. The first sub-code block mentioned above can also be understood as a code statement.
[0158] Since the result of the execution of the first sub-code block is used to determine the result of the execution of the first sub-code block, it can be seen that the source code file involves asynchronous functions, or the source code file is used to implement asynchronous tasks.
[0159] For example, after the above function f4 completes execution and provides a result, function f3 can be executed further. It is evident that the execution of task f3 depends on the result of task f4.
[0160] It's important to note that, according to the execution principle of asynchronous tasks in the Rust programming language, during the execution of each task, if the task is completed and ready to produce a result, the poll function will return Poll::Ready and provide the result. The task is then complete, and the next task can proceed. If a task encounters blocking behavior (e.g., unable to acquire a lock, network congestion, etc.), the poll function will return Poll::Pending. This execution will exit, but the task that encountered blocking behavior during this execution will be retained. When this task is scheduled again, the state machine will still maintain the state corresponding to that task. Therefore, in the next execution, one can enter the poll function from this state and continue executing the task corresponding to that state.
[0161] For example, if a blocking event occurred during the previous execution of the task in the first sub-block, the previous execution would have exited at the location of the first sub-block. In the next execution, it will continue from the first sub-block, enter the poll function, and continue executing the task corresponding to the first sub-block.
[0162] It is evident that the execution of the first sub-code block was blocked. The next execution can continue from the first sub-code block. If the task of the next execution is to obtain the call stack, then the call stack of the first sub-code block that was blocked in the previous execution can be obtained, that is, the function call relationship corresponding to the first sub-code block can be obtained, so as to make an accurate judgment on the running status of the coroutine.
[0163] It's easy to understand that the call stack of an asynchronous task typically goes through multiple await nodes. The node at the very end, or the one that might actually cause blocking behavior, is the leaf node. The first sub-code block mentioned above can be understood as a leaf node in the data structure.
[0164] S402 obtains the call stack by running the source code file at runtime, including the second statement.
[0165] It should be noted that when running the source code at runtime, the runtime includes a second statement, which is used to determine whether the task of this execution is to obtain the call stack.
[0166] For example, the second statement is as follows:
[0167]
[0168] When the source code file in step S401 above is executed at runtime, the runtime may include a second statement, which is a statement for returning to the stack. By instructing the first statement in the source code to retrieve the call stack, the second statement determines that the task of this execution is to retrieve the call stack when the source code file is executed at runtime, thereby recording the call stack and returning, thus completing the purpose of this execution.
[0169] It should be noted that the runtime does not directly run the source code file. It should be understood that the compiler compiles the source code file to obtain machine code, and the runtime runs the machine code obtained by the compiler from compiling the source code file.
[0170] Coroutine task logic is typically executed on a thread, meaning asynchronous tasks are performed on a thread. To avoid affecting the task logic when retrieving the call stack, a statement for pushing back the call stack can be added before the statement used to run the task logic.
[0171] In one implementation, the runtime also includes a third statement, which is used to execute the functionality of the first sub-code block, with the second statement preceding the third statement.
[0172] For example, the third statement is as follows:
[0173]
[0174] The second statement precedes the third statement, resulting in the following code file:
[0175]
[0176] As can be seen, the second statement used for pushing back the stack precedes the third statement used for executing the code block. If the task of this execution is to obtain the call stack, the push-back statement will be executed first, the call stack will be recorded, and the execution will exit. Obviously, this execution does not execute the function of the first sub-code block corresponding to the third statement, and therefore will not change the task data and state of the stackless coroutine.
[0177] Specifically, users can define the name and / or handle of the asynchronous task in the source code file, and determine that the run is to obtain the call stack by executing the handle or by specifying the name.
[0178] In one implementation, the first statement includes a first handle, specifically used to indicate the call stack of the first sub-code block by means of the first handle, and / or the first statement includes a first name, specifically used to indicate the call stack of the first sub-code block by means of the first name.
[0179] In one implementation, the first code block further includes a fourth statement, which defines a first handle to the first sub-code block and / or defines a first name to the first sub-code block.
[0180] For example, the fourth statement is as follows:
[0181] let handle=Runtime::spawn("TaskA",funcA())
[0182] Here, `handle` is the handle of the asynchronous task, and "TaskA" is the name of the asynchronous task. Therefore, the fourth statement is used by the user to define the name and / or handle of the asynchronous task.
[0183] The first statement can be `let backtrace = Runtime::get_backtrace(handle)`, which includes the handle of the asynchronous task. This handle indicates that the first statement is used to obtain the call stack of the asynchronous task.
[0184] The first statement can be `let backtrace = Runtime::get_backtrace("TaskA")`, which includes the name of the asynchronous task "TaskA". This name indicates that the first statement is used to obtain the call stack of the asynchronous task.
[0185] Generally speaking, after an asynchronous task has been running for a period of time, if a user wants to know the call stack of the asynchronous task, they can obtain the corresponding call stack from the runtime by executing the name or handle.
[0186] The method for obtaining the call stack provided in this application adds a push-back statement to the runtime code file of the Rust programming language, thereby increasing runtime control. The asynchronous task (or coroutine) is expanded on a thread. When the execution is for obtaining the call stack, the task on the thread is pushed back onto the stack, thus obtaining and recording the call stack. This execution process will not change the data and state of the stackless coroutine and will not affect the task logic. Clearly, the method for obtaining the call stack provided in this application does not require extensive modifications to the code file and does not incur significant runtime overhead to obtain the call stack, thereby avoiding impact on function calls or task logic.
[0187] Figure 5 This is a schematic diagram of a call stack acquisition device 500 provided in an embodiment of this application. The device 500 may possess the functions of the computer device described in the above method embodiments and can be used to execute the steps performed by the functions of the computer device in the above method embodiments. This function can be implemented in hardware, or in software, or by hardware executing corresponding software. The hardware or software includes one or more modules corresponding to the above functions.
[0188] In some examples, the processing module 510 is used to support the computer device in performing the processing actions in the above method embodiments, such as running source code files at runtime.
[0189] Optionally, the call stack acquisition device 500 may further include a storage unit 520 for storing the program code and data of the call stack acquisition device 500.
[0190] Figure 6 This is a schematic diagram of the structure of a computer device 600 provided in an embodiment of this application, as shown below. Figure 6 As shown, the computer device 600 includes at least one processor 610 and a transceiver 620. The processor 610 is coupled to a memory 630 and is used to execute instructions stored in the memory to control the transceiver 620 to transmit and / or receive signals.
[0191] Optionally, the computer device 600 also includes a memory 630 for storing instructions.
[0192] In some embodiments, the processor 610 and the memory 630 can be combined into a single processing device, with the processor 610 executing program code stored in the memory 630 to implement the aforementioned functions. Specifically, the memory 630 can be integrated into the processor 610 or independent of it.
[0193] In some embodiments, transceiver 620 may include a receiver and a transmitter.
[0194] The transceiver 620 may further include an antenna, and the number of antennas may be one or more. The transceiver 620 may be a communication interface or an interface circuit.
[0195] When the computer device 600 is a chip, the chip includes a transceiver module and a processing module. The transceiver module can be an input / output circuit or a communication interface; the processing module can be a processor, microprocessor, or integrated circuit integrated on the chip.
[0196] This application also provides a computer-readable storage medium storing computer instructions. When the computer instructions are executed on a computer device, the computer device performs the aforementioned method steps to implement the method for obtaining the call stack in the above embodiments.
[0197] This application also provides a computer program product that, when run on a computer, causes the computer to perform the aforementioned steps to implement the method for obtaining the call stack in the above embodiments.
[0198] Furthermore, embodiments of this application also provide an apparatus, which may specifically be a chip, component, or module. The apparatus may include a connected processor and a memory. The memory stores computer execution instructions. When the apparatus is running, the processor can execute the computer execution instructions stored in the memory, causing the chip to execute the method of obtaining the call stack in the above-described method embodiments.
[0199] Those skilled in the art will recognize that the units and algorithm steps of the various examples described in conjunction with the embodiments disclosed herein can be implemented in electronic hardware, or a combination of computer software and electronic hardware. Whether these functions are implemented in hardware or software depends on the specific application and design constraints of the technical solution. Those skilled in the art can use different methods to implement the described functions for each specific application, but such implementation should not be considered beyond the scope of this application.
[0200] Those skilled in the art will understand that, for the sake of convenience and brevity, the specific working processes of the systems, devices, and units described above can be referred to the corresponding processes in the foregoing method embodiments, and will not be repeated here.
[0201] In the several embodiments provided in this application, it should be understood that the disclosed systems, apparatuses, and methods can be implemented in other ways. For example, the apparatus embodiments described above are merely illustrative; for instance, the division of units is only a logical functional division, and in actual implementation, there may be other division methods. For example, multiple units or components may be combined or integrated into another system, or some features may be ignored or not executed. Furthermore, the coupling or direct coupling or communication connection shown or discussed may be through some interfaces; the indirect coupling or communication connection between apparatuses or units may be electrical, mechanical, or other forms.
[0202] The units described as separate components may or may not be physically separate. The components shown as units may or may not be physical units; that is, they may be located in one place or distributed across multiple network units. Some or all of the units can be selected to achieve the purpose of this embodiment according to actual needs.
[0203] In addition, the functional units in the various embodiments of this application can be integrated into one processing unit, or each unit can exist physically separately, or two or more units can be integrated into one unit.
[0204] If the aforementioned functions are implemented as software functional units and sold or used as independent products, they can be stored in a computer-readable storage medium. Based on this understanding, the technical solution of this application, in essence, or the part that contributes to the prior art, or a portion of the technical solution, can be embodied in the form of a software product. This computer software product is stored in a storage medium and includes several instructions to cause a computer device (which may be a personal computer, server, or network device, etc.) to execute all or part of the steps of the methods described in the various embodiments of this application. The aforementioned storage medium includes various media capable of storing program code, such as USB flash drives, portable hard drives, read-only memory (ROM), random access memory (RAM), magnetic disks, or optical disks.
[0205] The above description is merely a specific embodiment of this application, but the scope of protection of this application is not limited thereto. Any variations or substitutions that can be easily conceived by those skilled in the art within the scope of the technology disclosed in this application should be included within the scope of protection of this application. Therefore, the scope of protection of this application should be determined by the scope of the claims.< / t>
Claims
1. A method for obtaining the call stack, characterized in that, The method includes: Obtain a source code file, the source code file including a first statement, the first statement being used to instruct the acquisition of the call stack; The source code file is run by a runtime execution including the second statement, which is used to determine whether the task of this execution is to obtain the call stack.
2. The method according to claim 1, characterized in that, The source code file includes a first code block, the first code block includes a first sub-code block, the execution result of the first sub-code block is used to determine the execution result of the first code block, and the first code block includes the first statement.
3. The method according to claim 2, characterized in that, The runtime also includes a third statement, which is used to execute the function of the first sub-code block, and the second statement is located before the third statement.
4. The method according to claim 2 or 3, characterized in that, The first statement includes a first handle, specifically used to obtain the call stack of the first sub-code block by instructing the first handle; and / or The first statement includes a first name, and the first statement is specifically used to obtain the call stack of the first sub-code block by indicating the first name.
5. The method according to claim 4, characterized in that, The first code block further includes a fourth statement, which is used to define the first handle of the first sub-code block, and / or The fourth statement is used to define the first name of the first sub-code block.
6. The method according to any one of claims 2 to 5, characterized in that, The function of the first sub-code is one of the following: Semaphores, locks, networks, sleep.
7. The method according to any one of claims 1 to 6, characterized in that, The source code file is a code file in the Rust programming language.
8. A computer device, characterized in that, It includes a processor and a memory, the memory being used to store program instructions, and the processor being used to invoke the program instructions to perform the method of any one of claims 1 to 7.
9. An apparatus for obtaining a call stack, characterized in that, Includes modules for implementing the method of any one of claims 1 to 7.
10. A computer-readable storage medium, characterized in that, It stores a computer program thereon, which, when executed by a computer, enables the implementation of the method according to any one of claims 1 to 7.
11. A computer program product, characterized in that, Includes a computer program that, when executed by a processor, implements the method of any one of claims 1 to 7.
