| /******************************************************************************* |
| * Copyright (C) 2018 Cadence Design Systems, Inc. |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining |
| * a copy of this software and associated documentation files (the |
| * "Software"), to use this Software with Cadence processor cores only and |
| * not with any other processors and platforms, subject to |
| * the following conditions: |
| * |
| * The above copyright notice and this permission notice shall be included |
| * in all copies or substantial portions of the Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
| * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
| * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
| * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
| * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
| * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| |
| ******************************************************************************/ |
| |
| /******************************************************************************* |
| * xf-core.c |
| * |
| * DSP processing framework core |
| * |
| ******************************************************************************/ |
| |
| #define MODULE_TAG CORE |
| |
| /******************************************************************************* |
| * Includes |
| ******************************************************************************/ |
| |
| #include "xf.h" |
| |
| /******************************************************************************* |
| * Tracing tags |
| ******************************************************************************/ |
| |
| /* ...general initialization sequence */ |
| TRACE_TAG(INIT, 1); |
| |
| /* ...message dispatching */ |
| TRACE_TAG(DISP, 1); |
| |
| /* ...client registration procedures */ |
| TRACE_TAG(REG, 1); |
| |
| /* ...ports routing/unrouting */ |
| TRACE_TAG(ROUTE, 1); |
| |
| #ifdef XAF_PROFILE_DSP |
| /* ... MCPS/profile info */ |
| #include "xa_profiler.h" |
| #endif |
| /******************************************************************************* |
| * Internal helpers |
| ******************************************************************************/ |
| |
| /* ...translate client-id into component handle */ |
| static inline xf_component_t * xf_client_lookup(xf_core_data_t *cd, u32 client) |
| { |
| xf_cmap_link_t *link = &cd->cmap[client]; |
| |
| /* ...if link pointer is less than XF_CFG_MAX_CLIENTS, it is a free descriptor */ |
| return (link->next > XF_CFG_MAX_CLIENTS ? link->c : NULL); |
| } |
| |
| /* ...allocate client-id */ |
| static inline u32 xf_client_alloc(xf_core_data_t *cd) |
| { |
| u32 client = cd->free; |
| |
| /* ...advance list head to next free id */ |
| (client < XF_CFG_MAX_CLIENTS ? cd->free = cd->cmap[client].next : 0); |
| |
| return client; |
| } |
| |
| /* ...recycle client-id */ |
| static inline void xf_client_free(xf_core_data_t *cd, u32 client) |
| { |
| /* ...put client into the head of the free id list */ |
| cd->cmap[client].next = cd->free, cd->free = client; |
| } |
| |
| /******************************************************************************* |
| * Process commands to a proxy |
| ******************************************************************************/ |
| |
| /* ...register new client */ |
| static int xf_proxy_register(u32 core, xf_message_t *m) |
| { |
| xf_core_data_t *cd = XF_CORE_DATA(core); |
| u32 src = XF_MSG_SRC(m->id); |
| u32 client; |
| xf_component_t *component; |
| |
| /* ...allocate new client-id */ |
| XF_CHK_ERR((client = xf_client_alloc(cd)) != XF_CFG_MAX_CLIENTS, -EBUSY); |
| |
| /* ...create component via class factory */ |
| if ((component = xf_component_factory(core, m->buffer, m->length)) == NULL) |
| { |
| TRACE(ERROR, _x("Component creation failed")); |
| |
| /* ...recycle client-id */ |
| xf_client_free(cd, client); |
| |
| /* ...return generic out-of-memory code always (tbd) */ |
| return -ENOMEM; |
| } |
| |
| /* ...register component in a map */ |
| cd->cmap[client].c = component; |
| |
| /* ...set component "default" port specification ("destination") */ |
| component->id = __XF_PORT_SPEC(core, client, 0); |
| |
| /* ...adjust session-id to include newly created component-id */ |
| m->id = __XF_MSG_ID(src, component->id); |
| |
| /* ...do system-specific registration of component within IPC layer */ |
| xf_ipc_component_addref(m->id); |
| |
| TRACE(REG, _b("registered client: %u:%u (%s)"), core, client, (xf_id_t)m->buffer); |
| |
| /* ...and return success to remote proxy (zero-length output) */ |
| xf_response_ok(m); |
| |
| return 0; |
| } |
| |
| /* ...shared buffer allocation request */ |
| static int xf_proxy_alloc(u32 core, xf_message_t *m) |
| { |
| /* ...command is valid only if shared memory interface for core is specified */ |
| XF_CHK_ERR(xf_shmem_enabled(core), -EPERM); |
| |
| /* ...allocate shared memory buffer (system-specific function; may fail) */ |
| xf_shmem_alloc(core, m); |
| |
| /* ...pass result to remote proxy (on success buffer is non-null) */ |
| xf_response(m); |
| |
| return 0; |
| } |
| |
| /* ...shared buffer freeing request */ |
| static int xf_proxy_free(u32 core, xf_message_t *m) |
| { |
| /* ...command is valid only if shared memory interface for core is specified */ |
| XF_CHK_ERR(xf_shmem_enabled(core), -EPERM); |
| |
| /* ...pass buffer freeing request to system-specific function */ |
| xf_shmem_free(core, m); |
| |
| /* ...return success to remote proxy (function never fails) */ |
| xf_response(m); |
| |
| return 0; |
| } |
| |
| #if 0 |
| /* ...port routing command processing */ |
| static int xf_proxy_route(u32 core, xf_message_t *m) |
| { |
| xf_route_port_msg_t *cmd = m->buffer; |
| u32 src = cmd->src; |
| u32 dst = cmd->dst; |
| xf_component_t *component; |
| xf_output_port_t *port; |
| |
| /* ...source component must reside on the local core */ |
| XF_CHK_ERR(XF_MSG_SRC_CORE(src) == core, -EINVAL); |
| |
| /* ...make sure the "src" component is valid ("dst" may reside on other core) */ |
| if ((component = xf_client_lookup(XF_CORE_DATA(core), XF_PORT_CLIENT(src))) == NULL) |
| { |
| TRACE(ERROR, _x("Source port lookup failed: %x"), src); |
| return -ENOENT; |
| } |
| else if (!component->port || !(port = component->port(component, XF_PORT_ID(src)))) |
| { |
| TRACE(ERROR, _b("Source port doesn't exist: %x"), src); |
| return -ENOENT; |
| } |
| else if (xf_output_port_routed(port)) |
| { |
| TRACE(ERROR, _b("Source port is already routed: %x"), src); |
| return -EBUSY; |
| } |
| |
| /* ...route output port with source port set as destination */ |
| XF_CHK_API(xf_output_port_route(port, __XF_MSG_ID(dst, src), cmd->alloc_number, cmd->alloc_size, cmd->alloc_align)); |
| |
| TRACE(ROUTE, _b("Ports routed: %03x -> %03x"), src, dst); |
| |
| /* ...invoke component data-processing function directly (ignore errors? - tbd) */ |
| component->entry(component, NULL); |
| |
| /* ...return success result code (no output attached) */ |
| xf_response_ok(m); |
| |
| return 0; |
| } |
| |
| /* ...disconnect ports */ |
| static int xf_proxy_unroute(u32 core, xf_message_t *m) |
| { |
| xf_unroute_port_msg_t *cmd = m->buffer; |
| u32 src = cmd->src; |
| xf_component_t *component; |
| xf_output_port_t *port; |
| |
| /* ...source component must reside on the local core */ |
| XF_CHK_ERR(XF_MSG_SRC_CORE(src) == core, -EINVAL); |
| |
| /* ...lookup source (output) port */ |
| if ((component = xf_client_lookup(XF_CORE_DATA(core), XF_PORT_CLIENT(src))) == NULL) |
| { |
| TRACE(ERROR, _b("Source port lookup failed: %x"), src); |
| return -ENOENT; |
| } |
| else if (!component->port || !(port = component->port(component, XF_PORT_ID(src)))) |
| { |
| TRACE(ERROR, _b("Source port doesn't exist: %x"), src); |
| return -ENOENT; |
| } |
| else if (!xf_output_port_routed(port)) |
| { |
| /* ...port is not routed; satisfy immediately */ |
| goto done; |
| } |
| else if (!xf_output_port_idle(port)) |
| { |
| TRACE(ERROR, _b("Source port is not idle: %x"), src); |
| return -EBUSY; |
| } |
| |
| /* ...unroute port (call must succeed) */ |
| xf_output_port_unroute(port); |
| |
| /* ...we cannot satisfy the command now, and need to propagate it to a sink - tbd */ |
| //return 0; |
| |
| done: |
| /* ...pass success result code to caller */ |
| xf_response_ok(m); |
| |
| return 0; |
| } |
| #endif |
| |
| /* ...fill-this-buffer command processing */ |
| static int xf_proxy_output(u32 core, xf_message_t *m) |
| { |
| /* ...determine destination "client" */ |
| switch (XF_MSG_SRC_CLIENT(m->id)) |
| { |
| #if XF_TRACE_REMOTE |
| case 0: |
| /* ...destination is a tracer facility; submit buffer to tracer */ |
| xf_trace_submit(core, m); |
| return 0; |
| #endif |
| |
| default: |
| /* ...unrecognized destination; return general failure response */ |
| return XF_CHK_ERR(0, -EINVAL); |
| } |
| } |
| |
| /* ...flush command processing */ |
| static int xf_proxy_flush(u32 core, xf_message_t *m) |
| { |
| /* ...determine destination "client" */ |
| switch (XF_MSG_SRC_CLIENT(m->id)) |
| { |
| #if XF_TRACE_REMOTE |
| case 0: |
| /* ...destination is a tracer facility; flush current buffer */ |
| xf_trace_flush(core, m); |
| return 0; |
| #endif |
| |
| default: |
| /* ...unrecognized destination; return general failure response */ |
| return XF_CHK_ERR(0, -EINVAL); |
| } |
| } |
| |
| /* ...proxy command processing table */ |
| static int (* const xf_proxy_cmd[])(u32, xf_message_t *) = |
| { |
| [XF_OPCODE_TYPE(XF_REGISTER)] = xf_proxy_register, |
| [XF_OPCODE_TYPE(XF_ALLOC)] = xf_proxy_alloc, |
| [XF_OPCODE_TYPE(XF_FREE)] = xf_proxy_free, |
| #if 0 |
| [XF_OPCODE_TYPE(XF_ROUTE)] = xf_proxy_route, |
| [XF_OPCODE_TYPE(XF_UNROUTE)] = xf_proxy_unroute, |
| #endif |
| [XF_OPCODE_TYPE(XF_FILL_THIS_BUFFER)] = xf_proxy_output, |
| [XF_OPCODE_TYPE(XF_FLUSH)] = xf_proxy_flush, |
| }; |
| |
| /* ...total number of commands supported */ |
| #define XF_PROXY_CMD_NUM (sizeof(xf_proxy_cmd) / sizeof(xf_proxy_cmd[0])) |
| |
| /* ...process commands to a proxy */ |
| static void xf_proxy_command(u32 core, xf_message_t *m) |
| { |
| u32 opcode = m->opcode; |
| int res; |
| |
| /* ...dispatch command to proper hook */ |
| if (XF_OPCODE_TYPE(opcode) < XF_PROXY_CMD_NUM) |
| { |
| if ((res = xf_proxy_cmd[XF_OPCODE_TYPE(opcode)](core, m)) >= 0) |
| { |
| /* ...command processed successfully; do nothing */ |
| return; |
| } |
| } |
| else |
| { |
| TRACE(ERROR, _x("invalid opcode: %x"), opcode); |
| } |
| |
| /* ...command processing failed; return generic failure response */ |
| xf_response_err(m); |
| } |
| |
| /******************************************************************************* |
| * Message completion helper |
| ******************************************************************************/ |
| |
| /* ...put message into local IPC command queue on remote core (src != dst) */ |
| static inline void xf_msg_local_ipc_put(u32 src, u32 dst, xf_message_t *m) |
| { |
| xf_core_rw_data_t *rw = XF_CORE_RW_DATA(dst); |
| int first; |
| |
| /* ...flush message payload if needed */ |
| if (XF_LOCAL_IPC_NON_COHERENT) |
| { |
| /* ...it may be a command with output payload only - tbd */ |
| XF_PROXY_FLUSH(m->buffer, m->length); |
| } |
| |
| /* ...acquire mutex to target rw-data (running on source core) */ |
| xf_mutex_lock(src); |
| |
| /* ...assure memory coherency as needed */ |
| if (XF_LOCAL_IPC_NON_COHERENT) |
| { |
| /* ...invalidate local queue data */ |
| XF_PROXY_INVALIDATE(&rw->local, sizeof(rw->local)); |
| |
| /* ...place message into queue */ |
| first = xf_msg_enqueue(&rw->local, m); |
| |
| /* ...flush both queue and message data */ |
| XF_PROXY_FLUSH(&rw->local, sizeof(rw->local)), XF_PROXY_FLUSH(m, sizeof(*m)); |
| } |
| else |
| { |
| /* ...just enqueue the message */ |
| first = xf_msg_enqueue(&rw->local, m); |
| } |
| |
| /* ...release global rw-memory access lock */ |
| xf_mutex_unlock(src); |
| |
| /* ...signal IPI interrupt on destination core as needed */ |
| (first ? xf_ipi_assert(dst), 1 : 0); |
| } |
| |
| /* ...dequeue message from core-specific dispatch queue */ |
| static inline xf_message_t * xf_msg_local_ipc_get(u32 core) |
| { |
| xf_core_rw_data_t *rw = XF_CORE_RW_DATA(core); |
| xf_message_t *m; |
| |
| /* ...retrieve message from queue in atomic fashion */ |
| xf_mutex_lock(core); |
| |
| /* ...process memory coherency as required */ |
| if (XF_LOCAL_IPC_NON_COHERENT) |
| { |
| /* ...inavlidate local rw-data */ |
| XF_PROXY_INVALIDATE(&rw->local, sizeof(rw->local)); |
| |
| /* ...get message from the queue */ |
| if ((m = xf_msg_dequeue(&rw->local)) != NULL) |
| { |
| /* ...flush rw-queue data */ |
| XF_PROXY_FLUSH(&rw->local, sizeof(rw->local)); |
| } |
| } |
| else |
| { |
| /* ...just dequeue message from the queue */ |
| m = xf_msg_dequeue(&rw->local); |
| } |
| |
| /* ...release rw-memory access lock */ |
| xf_mutex_unlock(core); |
| |
| /* ...invalidate message header and data as needed */ |
| if (XF_LOCAL_IPC_NON_COHERENT && m != NULL) |
| { |
| /* ...invalidate message header */ |
| XF_PROXY_INVALIDATE(m, sizeof(*m)); |
| |
| /* ...and data if needed (it may not be always needed - tbd) */ |
| (m->length ? XF_PROXY_INVALIDATE(m->buffer, m->length) : 0); |
| } |
| |
| /* ...return message */ |
| return m; |
| } |
| |
| /* ...retrieve message from local queue (protected from ISR) */ |
| static inline int xf_msg_local_put(u32 core, xf_message_t *m) |
| { |
| xf_core_data_t *cd = XF_CORE_DATA(core); |
| int first; |
| u32 status; |
| |
| /* ...use interrupt masking protocol to protect message queue */ |
| status = xf_isr_disable(core); |
| first = xf_msg_enqueue(&cd->queue, m); |
| xf_isr_restore(core, status); |
| |
| return first; |
| } |
| |
| /* ...retrieve message from local queue (protected from ISR) */ |
| static inline xf_message_t * xf_msg_local_get(u32 core) |
| { |
| xf_core_data_t *cd = XF_CORE_DATA(core); |
| xf_message_t *m; |
| u32 status; |
| |
| /* ...use interrupt masking protocol to protect message queue */ |
| status = xf_isr_disable(core); |
| m = xf_msg_dequeue(&cd->queue); |
| xf_isr_restore(core, status); |
| |
| return m; |
| } |
| |
| /* ...retrieve message from local queue (protected from ISR) */ |
| static inline xf_message_t * xf_msg_local_response_get(u32 core) |
| { |
| xf_core_data_t *cd = XF_CORE_DATA(core); |
| xf_message_t *m; |
| u32 status; |
| |
| /* ...use interrupt masking protocol to protect message queue */ |
| status = xf_isr_disable(core); |
| m = xf_msg_dequeue(&cd->response); |
| xf_isr_restore(core, status); |
| |
| return m; |
| } |
| |
| /* ...call component data processing function */ |
| static inline void xf_core_process(xf_component_t *component) |
| { |
| u32 id = component->id; |
| |
| /* ...client look-up successfull */ |
| TRACE(DISP, _b("core[%u]::client[%u]::process"), XF_PORT_CORE(id), XF_PORT_CLIENT(id)); |
| |
| /* ...call data-processing interface */ |
| if (component->entry(component, NULL) < 0) |
| { |
| TRACE(ERROR, _b("execution error (ignored)")); |
| } |
| } |
| |
| /* ...dispatch message queue execution */ |
| static inline void xf_core_dispatch(xf_core_data_t *cd, u32 core, xf_message_t *m) |
| { |
| u32 client; |
| xf_component_t *component; |
| |
| /* ...do client-id/component lookup */ |
| if (XF_MSG_DST_PROXY(m->id)) |
| { |
| TRACE(DISP, _b("core[%u]::proxy-cmd(id=%x, opcode=%x)"), core, m->id, m->opcode); |
| |
| /* ...process message addressed to proxy */ |
| xf_proxy_command(core, m); |
| |
| /* ...do not like this return statement... - tbd */ |
| return; |
| } |
| |
| /* ...message goes to local component */ |
| client = XF_MSG_DST_CLIENT(m->id); |
| |
| /* ...check if client is alive */ |
| if ((component = xf_client_lookup(cd, client)) != NULL) |
| { |
| /* ...client look-up successfull */ |
| TRACE(DISP, _b("core[%u]::client[%u]::cmd(id=%x, opcode=%x)"), core, client, m->id, m->opcode); |
| |
| /* ...pass message to component entry point */ |
| if (component->entry(component, m) < 0) |
| { |
| /* ...call component destructor */ |
| if (component->exit(component, m) == 0) |
| { |
| /* ...component cleanup completed; recycle component-id */ |
| xf_client_free(cd, client); |
| |
| /* ...do system-specific deregistration of component within IPC layer */ |
| xf_ipc_component_rmref(__XF_PORT_SPEC(core, client, 0)); |
| } |
| } |
| } |
| else |
| { |
| TRACE(DISP, _b("Discard message id=%x - client %u:%u not registered"), m->id, core, client); |
| |
| /* ...complete message with general failure response */ |
| xf_response_err(m); |
| } |
| } |
| |
| /******************************************************************************* |
| * Entry points |
| ******************************************************************************/ |
| |
| /* ...submit message for instant execution on some core */ |
| void xf_msg_submit(xf_message_t *m) |
| { |
| u32 src = XF_MSG_SRC_CORE(m->id); |
| u32 dst = XF_MSG_DST_CORE(m->id); |
| |
| /* ...check if message shall go through local IPC layer */ |
| if (src ^ dst) |
| { |
| /* ...put message into local IPC queue */ |
| xf_msg_local_ipc_put(src, dst, m); |
| } |
| else |
| { |
| /* ...message is addressed to same core */ |
| xf_msg_local_put(src, m); |
| } |
| } |
| |
| /* ...complete message and pass response to a caller */ |
| void xf_msg_complete(xf_message_t *m) |
| { |
| u32 src = XF_MSG_SRC(m->id); |
| u32 dst = XF_MSG_DST(m->id); |
| |
| /* ...swap src/dst specifiers */ |
| m->id = __XF_MSG_ID(dst, src); |
| |
| /* ...check if message goes to remote IPC layer */ |
| if (XF_MSG_DST_PROXY(m->id)) |
| { |
| /* ...return message to proxy */ |
| xf_msg_proxy_complete(m); |
| } |
| else |
| { |
| /* ...destination is within DSP cluster; check if that is a data buffer */ |
| switch (m->opcode) |
| { |
| case XF_EMPTY_THIS_BUFFER: |
| /* ...emptied buffer goes back to the output port */ |
| m->opcode = XF_FILL_THIS_BUFFER; |
| break; |
| |
| case XF_FILL_THIS_BUFFER: |
| /* ...filled buffer is passed to the input port */ |
| m->opcode = XF_EMPTY_THIS_BUFFER; |
| break; |
| } |
| |
| /* ...submit message for execution */ |
| xf_msg_submit(m); |
| } |
| } |
| |
| /* ...initialize per-core framework data */ |
| int xf_core_init(u32 core) |
| { |
| xf_core_data_t *cd = XF_CORE_DATA(core); |
| xf_cmap_link_t *link; |
| u32 i; |
| |
| /* ...create list of free client descriptors */ |
| for (link = &cd->cmap[i = 0]; i < XF_CFG_MAX_CLIENTS; i++, link++) |
| { |
| link->next = i + 1; |
| } |
| |
| /* ...set head of free clients list */ |
| cd->free = 0; |
| |
| /* ...initialize local queue scheduler */ |
| xf_sched_init(&cd->sched); |
| |
| /* ...initialize IPI subsystem */ |
| XF_CHK_API(xf_ipi_init(core)); |
| |
| /* ...initialize shared read-write memory */ |
| XF_CHK_API(xf_shmem_enabled(core) ? xf_shmem_init(core) : 0); |
| |
| /* ...initialize scratch memory */ |
| XF_CHK_ERR(cd->scratch = xf_scratch_mem_init(core), -ENOMEM); |
| |
| /* ...okay... it's all good */ |
| TRACE(INIT, _b("core-%u initialized"), core); |
| |
| return 0; |
| } |
| |
| /* ...core executive loop function */ |
| void xf_core_service(u32 core) |
| { |
| xf_core_data_t *cd = &xf_core_data[core]; |
| u32 status; |
| xf_message_t *m; |
| xf_task_t *t; |
| |
| #ifdef XAF_PROFILE_DSP |
| START_TIME_XA_PROFILER(prof); |
| #endif |
| do |
| { |
| /* ...clear local status change */ |
| status = 0; |
| |
| /* ...if core is servicing shared memory with AP, do it first - actually, they all need to support it */ |
| if (xf_shmem_enabled(core)) |
| { |
| /* ...process all commands */ |
| xf_shmem_process_queues(core); |
| } |
| |
| /* ...check if we have a backlog message placed into interim queue */ |
| while ((m = xf_msg_local_ipc_get(core)) || (m = xf_msg_local_get(core))) |
| { |
| /* ...dispatch message execution */ |
| xf_core_dispatch(cd, core, m); |
| |
| /* ...set local status change */ |
| status = 1; |
| } |
| |
| /* ...check if we have pending responses (submitted from ISR) we need to process */ |
| while ((m = xf_msg_local_response_get(core)) != NULL) |
| { |
| /* ...call completion handler on current stack */ |
| xf_msg_complete(m); |
| |
| /* ...set local status change */ |
| status = 1; |
| |
| } |
| |
| /* ...if scheduler queue is empty, break the loop and pause the core */ |
| if ((t = xf_sched_get(&cd->sched)) != NULL) |
| { |
| /* ...data-processing execution (ignore internal errors) */ |
| xf_core_process((xf_component_t *)t); |
| |
| /* ...set local status change */ |
| status = 1; |
| } |
| } |
| while (status); |
| |
| #ifdef XAF_PROFILE_DSP |
| STOP_TIME_XA_PROFILER(prof); |
| |
| if(prof.g_output_bytes) |
| { |
| unsigned long output_samples = prof.g_output_bytes; |
| output_samples >>= (prof.channels == 2 ? 1 : 0); |
| output_samples >>= (prof.pcm_width == 24 ? 2 : 1); |
| |
| COMPUTE_MHZ_XA_PROFILER(prof, output_samples, prof.sample_rate, 0); |
| |
| prof.g_output_bytes = prof.cycles = 0; /* reset counters */ |
| } |
| #endif |
| |
| } |
| |
| /* ...global data initialization function */ |
| int xf_global_init(void) |
| { |
| /* ...what global data we have to initialize? - tbd */ |
| TRACE(INIT, _b("Global data initialized")); |
| |
| return 0; |
| } |