2009-03-09 15 views
8

Estoy trabajando en una biblioteca que admite entornos de programación múltiple como VB6 y FoxPro. Tengo que seguir con la convención de C ya que es el denominador común más bajo. Ahora tengo una pregunta sobre el estilo.Función de API de estilo de C

Supongamos que la función procesa la entrada y devuelve una cadena. Durante el proceso, el error puede suceder. El estilo actual propuesta es la siguiente:

int func(input params... char* buffer, unsigned int* buffer_size); 

Lo bueno de este estilo es que todo está incluido en el prototipo, incluyendo el código de error. Y la asignación de memoria puede evitarse. El problema es que la función es bastante detallada. Y como el buffer_size puede ser cualquiera, requiere más código para implementar.

Otra opción es volver char *, y devolver NULL para indicar error:

char* func(input params...); 

Este estilo requiere de llamadas para borrar la memoria intermedia. Se requiere asignación de memoria para que un programa servidor pueda enfrentar problemas de fragmentación de memoria.

Una variante de la segunda opción es usar una variable local de subproceso para mantener el puntero devuelto char *, para que el usuario no tenga que eliminar el búfer.

¿Qué estilo te gusta? ¿Y la razón?

+0

¿No debería el tipo de buffer ser char **? Además, ¿por qué necesita un buffer_size en la opción uno y no en la opción dos? – mweerden

+0

Pasa un búfer preasignado como el parámetro in y espera que la función llamada lo complete con un texto de error. – sharptooth

+0

Ok, pero luego buffer_size no necesita ser un puntero, ¿verdad? – mweerden

Respuesta

1

La segunda variante es más limpia.

COM IErrorInfo es una implementación del segundo enfoque. El servidor llama a SetErrorInfo para establecer detalles de lo que salió mal y devuelve un código de error. La persona que llama examina el código y puede llamar a GetErrorInfo para obtener los detalles. La persona que llama es responsable de liberar IErrorInfo, pero pasar los parámetros de cada llamada en la primera variante tampoco es bello.

El servidor podría preasignar suficiente memoria en el inicio para que seguramente tenga suficiente memoria para devolver los detalles del error.

2

Si tengo que elegir entre los dos estilos, iría por el primero cada vez. El segundo estilo le da a los usuarios de su biblioteca algo más en lo que pensar, la asignación de memeory, y alguien se olvidará de liberar la memoria.

5

Preferiría la primera definición, donde se transfieren el búfer y su tamaño. Hay excepciones, pero generalmente no se espera que tenga que limpiar después de las funciones que llama. Mientras que si asigno la memoria y la paso a una función, entonces sé que debo limpiarla después de mí mismo.

Manejar buffers de diferentes tamaños no debería ser un gran problema.

+0

Esto es en gran medida cómo lo hace la API de Windows, por lo que es lógico emular eso. –

2

Otro problema con el segundo estilo es que los contextos a los que se asigna la memoria pueden ser diferentes. Por ejemplo:

// your library in C 
char * foo() { 
    return malloc(100); 
} 

// my client code C++ 
char * p = foo();  // call your code 
delete p;    // natural for me to do, but ... aaargh! 

Y esto es solo una pequeña parte del problema. Puede decir que ambos lados deben usar malloc & gratis, pero ¿y si están usando implementaciones de compilador diferentes? Es mejor que todas las asignaciones y desasignaciones ocurran en el mismo lugar. si esta es la biblioteca, el código del cliente depende de usted.

1

La primera edición sería menos propensa a errores cuando otros programadores la usan.

Si los programadores tienen que asignar memoria ellos mismos, es más probable que recuerden liberarla. Si una biblioteca les asigna memoria, es otra abstracción y puede/dará lugar a complicaciones.

1

Pocas cosas para considerar;

  • Asignación y desasignación deben ocurrir en el mismo alcance (idealmente). Lo mejor es pasar en un búfer preasignado por la persona que llama. La persona que llama puede liberar esto de forma segura más adelante. Esto plantea la pregunta: ¿qué tan grande debe ser el buffer? Un enfoque que he visto utilizado bastante ampliamente en Win32 es pasar NULL como el búfer de entrada y el parámetro size le dirá cuánto necesita.

  • ¿Cuántas condiciones posibles de error supervisa? Devolver un char* puede limitar el alcance de los informes de errores.

  • ¿Qué condiciones previas y posteriores desea que se cumplan? ¿Tu prototipo refleja eso?

  • ¿Hace usted una comprobación de errores en la persona que llama o en quien llama?

Realmente no puedo decir que uno es mejor que el otro, ya que no tengo una idea general. Pero estoy seguro de que estas cosas pueden comenzar a pensar tan bien como las otras publicaciones.

8

Soy un poco "bienes dañados" cuando se trata de este tema. Solía ​​diseñar y mantener API bastante grandes para telecomunicaciones integradas. Un contexto donde no puedes dar nada por hecho. Ni siquiera cosas como variables globales o TLS. A veces, incluso los almacenamientos intermedios de montón aparecen que en realidad están dirigidos a la memoria ROM.

Por lo tanto, si está buscando un "mínimo común denominador", también puede pensar qué construcciones de lenguaje están disponibles en su entorno objetivo (es probable que el compilador acepte cualquier cosa dentro del estándar C, pero si no es compatible, el enlazador dirá que no).

Habiendo dicho eso, Siempre iré por la alternativa 1. En parte porque (como otros han señalado), nunca se debe asignar memoria para el usuario directamente (un enfoque indirecto se explica más abajo). Incluso si se garantiza que el usuario trabajará con C pura y simple, todavía podría, por ejemplo, usar su propia API de administración de memoria personalizada para rastrear fugas, registro de diagnóstico, etc. El apoyo para estrategias como esa es comúnmente apreciado.

La comunicación de error es una de las cosas más importantes cuando se trata de una API. Dado que el usuario probablemente tenga formas distintas de manejar los errores en su código, debe ser lo más constante posible acerca de esta comunicación a través de la API. El usuario debería ser capaz de ajustar el manejo de errores hacia su API de manera consistente y con un código mínimo. En general, siempre recomendaría usar códigos de enum claros o define/typedefs. Yo personalmente prefiero typedef: ed enumeraciones:

typedef enum { 

    RESULT_ONE, 
    RESULT_TWO 

} RESULT; 

.. porque proporciona seguridad de tipo/asignación.

Tener un obtener el último error función también es bueno (requiere almacenamiento central sin embargo), personalmente lo uso únicamente para proporcionar información adicional sobre un error ya reconocido.

El nivel de detalle de la alternativa 1 se puede limitar al hacer compuestos simples como esto:

struct Buffer 
{ 
    unsigned long size; 
    char* data; 
}; 

A continuación, el API puede verse mejor:

ERROR_CODE func(params... , Buffer* outBuffer); 

Esta estrategia también se abre para más elaborado mecanismos. Digamos por ejemplo, usted debe ser capaz de asignar memoria para el usuario (por ejemplo, si necesita cambiar el tamaño de la memoria intermedia), entonces se puede proporcionar una aproximación indirecta a esto:

struct Buffer 
{ 
    unsigned long size; 
    char* data; 
    void* (*allocator_callback)(unsigned long size); 
    void (*free_callback)(void* p); 
}; 

Por supuesto, el estilo de este tipo de construcciones es siempre abierto para un debate serio.

¡Buena suerte!

+0

¿No podría pasar esa estructura de búfer copiando en la pila en su lugar. Es un poco estúpido usar memoria dinámica para eso (supongo que sí) – toto

+1

@toto: Tenga en cuenta que el búfer no tiene que ser memoria dinámica solo porque la función toma un puntero. Podría ser una instancia de pila pasada por una dirección. Sin embargo, generalmente recomendaría usar una estructura de Buffer como esta como un tipo general, en lugar de ajustar el tamaño y los datos en una estructura de pila cada vez que necesita pasarla a una función. Supongo que eso es lo que estabas pensando acerca de las estructuras declaradas de pila. – sharkin

+0

Sí, lo siento, es solo un daño de Java sobrante que tengo. Un puntero está bien para un objeto en la pila. – toto

0

lo haría de manera similar a la primera forma, pero sólo sutilmente diferente, según el modelo de snprintf y funciones similares:

int func(char* buffer, size_t buffer_size, input params...); 

De esta manera, si usted tiene un montón de ellos, pueden ser similares , y puede usar números variables de argumentos siempre que sea útil.

Estoy de acuerdo en gran medida con las razones expuestas ya-para el uso de la versión 1 en lugar de la versión 2 - problemas de memoria son mucho más probable con la versión 2.

+0

No creo que se estuviera refiriendo a la elipsis usando el '...'. Si no es por otra cosa, la elipsis siempre debe ser la última en la lista de argumentos. – sharkin

+0

Estoy seguro de que no se estaba refiriendo a eso, pero me dio la idea de reorganizar los parámetros. =) –

1

¿Qué pasa con el uso de ambos métodos? Estoy de acuerdo con el consenso de las respuestas que favorecen el estilo 1 frente a las trampas de estilo 2. Me siento estilo 2 podría utilizarse si todo de su API sigue un lenguaje de nomenclatura coherente, así:


// Style 1 functions 
int fooBuff(char* buffer, unsigned int buffer_size, input params...); 

// Style 2 functions 
char* fooBuffAlloc(input params...); 
bool fooBuffFree(char* foo); 

/D

Cuestiones relacionadas