diff options
| author | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-09-14 13:56:49 +0200 |
|---|---|---|
| committer | jwijenbergh <jeroenwijenbergh@protonmail.com> | 2022-09-14 13:56:49 +0200 |
| commit | da83f54606c9c1d2786d87074ee17ed972d2e1b2 (patch) | |
| tree | 0be57934f9f467c87576abb0b457fb54b2d25d52 /exports | |
| parent | fd34e72da8c604517050ada7e883ba982829d985 (diff) | |
Refactor: Return without json
Diffstat (limited to 'exports')
| -rw-r--r-- | exports/Makefile | 12 | ||||
| -rw-r--r-- | exports/c/common.c | 6 | ||||
| -rw-r--r-- | exports/c/common.h | 3 | ||||
| -rw-r--r-- | exports/c/disco.h | 15 | ||||
| -rw-r--r-- | exports/c/servers.h | 43 | ||||
| -rw-r--r-- | exports/common.mk | 2 | ||||
| -rw-r--r-- | exports/disco.go | 97 | ||||
| -rw-r--r-- | exports/exports.go | 66 | ||||
| -rw-r--r-- | exports/servers.go | 273 |
9 files changed, 482 insertions, 35 deletions
diff --git a/exports/Makefile b/exports/Makefile index b833228..46d17a9 100644 --- a/exports/Makefile +++ b/exports/Makefile @@ -2,6 +2,8 @@ include common.mk +CLIBPATH=./c + ifeq ($(LIB_SUFFIX),.so) # Add SONAME as cgo does not currently do this. Mostly for Android, see https://stackoverflow.com/a/48291044 export override CGO_LDFLAGS += -Wl,-soname,$(LIB_FILE) @@ -13,12 +15,18 @@ ifdef COPY_LIB_TO install $< -Dt $(COPY_LIB_TO) endif +${CLIBPATH}/libcommon$(LIB_SUFFIX): ${CLIBPATH}/common.c + $(CC) -c -Wall -Werror -fpic -o ${CLIBPATH}/common.o ${CLIBPATH}/common.c + $(CC) -shared -o $@ ${CLIBPATH}/common.o + # Build shared library and remove lib prefix (if any) from header name # GOOS and GOARCH envvars are set by common.mk # This extra target prevents unnecessary rebuild -lib/$(GOOS)/$(GOARCH)/$(LIB_FILE): exports.go .. - CGO_ENABLED=1 go build -o $@ -buildmode=c-shared $< +lib/$(GOOS)/$(GOARCH)/$(LIB_FILE): ${CLIBPATH}/libcommon$(LIB_SUFFIX) exports.go servers.go .. + CGO_ENABLED=1 go build -o $@ -buildmode=c-shared . mv lib/$(GOOS)/$(GOARCH)/$(LIB_PREFIX)$(LIB_NAME).h lib/$(GOOS)/$(GOARCH)/$(LIB_NAME).h || true # Normalize header name clean: rm -rf ../exports/lib/* + rm -rf ${CLIBPATH}/common.o + rm -rf ${CLIBPATH}/libcommon.so diff --git a/exports/c/common.c b/exports/c/common.c new file mode 100644 index 0000000..425a459 --- /dev/null +++ b/exports/c/common.c @@ -0,0 +1,6 @@ +#include "common.h" + +void call_callback(PythonCB callback, const char *name, int oldstate, int newstate, void* data) +{ + callback(name, oldstate, newstate, data); +} diff --git a/exports/c/common.h b/exports/c/common.h new file mode 100644 index 0000000..068ad4c --- /dev/null +++ b/exports/c/common.h @@ -0,0 +1,3 @@ +typedef void (*PythonCB)(const char* name, int oldstate, int newstate, void* data); + +void call_callback(PythonCB callback, const char *name, int oldstate, int newstate, void* data); diff --git a/exports/c/disco.h b/exports/c/disco.h new file mode 100644 index 0000000..41d59fa --- /dev/null +++ b/exports/c/disco.h @@ -0,0 +1,15 @@ +// for size_t +#include <stddef.h> + +typedef struct discoveryOrganization { + const char* display_name; + const char* org_id; + const char* secure_internet_home; + const char* keyword_list; +} discoveryOrganization; + +typedef struct discoveryOrganizations { + unsigned long long int version; + discoveryOrganization** organizations; + size_t total_organizations; +} discoveryOrganizations; diff --git a/exports/c/servers.h b/exports/c/servers.h new file mode 100644 index 0000000..39e52a2 --- /dev/null +++ b/exports/c/servers.h @@ -0,0 +1,43 @@ +// for size_t +#include <stddef.h> + +// The struct for a single server profile +typedef struct serverProfile { + const char* id; + const char* display_name; + //const char* proto_list; + int default_gateway; +} serverProfile; + +// The struct for all server profiles +typedef struct serverProfiles { + int current; + serverProfile** profiles; + size_t total_profiles; +} serverProfiles; + +// The struct for server locations +typedef struct serverLocations { + const char** locations; + size_t total_locations; +} serverLocations; + +// The struct for a single server +typedef struct server { + const char* identifier; + const char* display_name; + const char* country_code; + const char** support_contact; + size_t total_support_contact; + serverProfiles* profiles; + unsigned long long int expire_time; +} server; + +// The struct for all servers +typedef struct servers { + server** custom_servers; + size_t total_custom; + server** institute_servers; + size_t total_institute; + server* secure_internet_server; +} servers; diff --git a/exports/common.mk b/exports/common.mk index c1e2a7e..211d460 100644 --- a/exports/common.mk +++ b/exports/common.mk @@ -7,6 +7,8 @@ ifndef GOARCH export GOARCH := $(shell go env GOHOSTARCH) endif +CC = gcc + ifeq (windows,$(GOOS)) LIB_PREFIX ?= LIB_SUFFIX ?= .dll diff --git a/exports/disco.go b/exports/disco.go new file mode 100644 index 0000000..9ee2af9 --- /dev/null +++ b/exports/disco.go @@ -0,0 +1,97 @@ +package main + +/* +// for free +#include <stdlib.h> +#include "c/disco.h" +*/ +import "C" + +import ( + "unsafe" + + "github.com/jwijenbergh/eduvpn-common" + "github.com/jwijenbergh/eduvpn-common/internal/types" +) + +func getCPtrDiscoOrganization( + state *eduvpn.VPNState, + organization *types.DiscoveryOrganization, +) *C.discoveryOrganization { + returnedStruct := (*C.discoveryOrganization)( + C.malloc(C.size_t(unsafe.Sizeof(C.discoveryOrganization{}))), + ) + returnedStruct.display_name = C.CString(state.GetTranslated(organization.DisplayName)) + returnedStruct.org_id = C.CString(organization.OrgId) + returnedStruct.secure_internet_home = C.CString(organization.SecureInternetHome) + returnedStruct.keyword_list = C.CString(state.GetTranslated(organization.KeywordList)) + return returnedStruct +} + +func getCPtrDiscoOrganizations( + state *eduvpn.VPNState, + organizations *types.DiscoveryOrganizations, +) (C.size_t, **C.discoveryOrganization) { + totalOrganizations := C.size_t(len(organizations.List)) + var organizationsPtr **C.discoveryOrganization + if totalOrganizations > 0 { + organizationsPtr = (**C.discoveryOrganization)( + C.malloc(totalOrganizations * C.size_t(unsafe.Sizeof(uintptr(0)))), + ) + cOrganizations := (*[1<<30 - 1]*C.discoveryOrganization)(unsafe.Pointer(organizationsPtr))[:totalOrganizations:totalOrganizations] + index := 0 + for _, organization := range organizations.List { + cOrganization := getCPtrDiscoOrganization(state, &organization) + cOrganizations[index] = cOrganization + index += 1 + } + } + return totalOrganizations, organizationsPtr +} + +func freeDiscoOrganization(cOrganization *C.discoveryOrganization) { + C.free(unsafe.Pointer(cOrganization.display_name)) + C.free(unsafe.Pointer(cOrganization.org_id)) + C.free(unsafe.Pointer(cOrganization.secure_internet_home)) + C.free(unsafe.Pointer(cOrganization.keyword_list)) + C.free(unsafe.Pointer(cOrganization)) +} + +//export FreeDiscoOrganizations +func FreeDiscoOrganizations(cOrganizations *C.discoveryOrganizations) { + if cOrganizations.total_organizations > 0 { + organizations := (*[1<<30 - 1]*C.discoveryOrganization)(unsafe.Pointer(cOrganizations.organizations))[:cOrganizations.total_organizations:cOrganizations.total_organizations] + for i := C.size_t(0); i < cOrganizations.total_organizations; i++ { + freeDiscoOrganization(organizations[i]) + } + C.free(unsafe.Pointer(cOrganizations.organizations)) + } + C.free(unsafe.Pointer(cOrganizations)) +} + +//export GetDiscoOrganizations +func GetDiscoOrganizations(name *C.char) *C.discoveryOrganizations { + nameStr := C.GoString(name) + state, stateErr := GetVPNState(nameStr) + // TODO + if stateErr != nil { + panic(stateErr) + } + organizations, organizationsErr := state.GetDiscoOrganizations() + // TODO + if organizationsErr != nil { + panic(organizationsErr) + } + + returnedStruct := (*C.discoveryOrganizations)( + C.malloc(C.size_t(unsafe.Sizeof(C.discoveryOrganizations{}))), + ) + + returnedStruct.version = C.ulonglong(organizations.Version) + returnedStruct.total_organizations, returnedStruct.organizations = getCPtrDiscoOrganizations( + state, + organizations, + ) + + return returnedStruct +} diff --git a/exports/exports.go b/exports/exports.go index b4eb909..797a192 100644 --- a/exports/exports.go +++ b/exports/exports.go @@ -1,15 +1,13 @@ package main /* -#include <stdlib.h> - -typedef void (*PythonCB)(const char* name, int oldstate, int newstate, const char* data); +#cgo CFLAGS: -I${SRCDIR}/c +#cgo LDFLAGS: -Wl,-rpath,${SRCDIR}/c +#cgo LDFLAGS: -L${SRCDIR}/c +#cgo LDFLAGS: -lcommon -__attribute__((weak)) -void call_callback(PythonCB callback, const char *name, int oldstate, int newstate, const char* data) -{ - callback(name, oldstate, newstate, data); -} +#include <stdlib.h> +#include "c/common.h" */ import "C" @@ -26,7 +24,28 @@ var P_StateCallbacks map[string]C.PythonCB var VPNStates map[string]*eduvpn.VPNState +func GetStateData( + state *eduvpn.VPNState, + stateID eduvpn.FSMStateID, + data interface{}, +) unsafe.Pointer { + switch stateID { + case eduvpn.STATE_NO_SERVER: + return (unsafe.Pointer)(getTransitionDataServers(state, data)) + case eduvpn.STATE_OAUTH_STARTED: + if converted, ok := data.(string); ok { + return (unsafe.Pointer)(C.CString(converted)) + } + case eduvpn.STATE_ASK_LOCATION: + return (unsafe.Pointer)(getTransitionSecureLocations(data)) + default: + return nil + } + return nil +} + func StateCallback( + state *eduvpn.VPNState, name string, old_state eduvpn.FSMStateID, new_state eduvpn.FSMStateID, @@ -39,18 +58,10 @@ func StateCallback( name_c := C.CString(name) oldState_c := C.int(old_state) newState_c := C.int(new_state) - data_json, jsonErr := json.Marshal(data) - var dataJsonString string - if jsonErr != nil { - // TODO: How to handle error further? Log? - dataJsonString = "{}" - } else { - dataJsonString = string(data_json) - } - data_c := C.CString(dataJsonString) + data_c := GetStateData(state, new_state, data) C.call_callback(P_StateCallback, name_c, oldState_c, newState_c, data_c) C.free(unsafe.Pointer(name_c)) - C.free(unsafe.Pointer(data_c)) + // data_c gets freed by the wrapper } func GetVPNState(name string) (*eduvpn.VPNState, error) { @@ -87,7 +98,7 @@ func Register( nameStr, C.GoString(config_directory), func(old eduvpn.FSMStateID, new eduvpn.FSMStateID, data interface{}) { - StateCallback(nameStr, old, new, data) + StateCallback(state, nameStr, old, new, data) }, debug != 0, ) @@ -150,7 +161,7 @@ func getConfigJSON(config string, configType string) *C.char { } //export RemoveSecureInternet -func RemoveSecureInternet(name *C.char) (*C.char) { +func RemoveSecureInternet(name *C.char) *C.char { nameStr := C.GoString(name) state, stateErr := GetVPNState(nameStr) if stateErr != nil { @@ -161,7 +172,7 @@ func RemoveSecureInternet(name *C.char) (*C.char) { } //export RemoveInstituteAccess -func RemoveInstituteAccess(name *C.char, url *C.char) (*C.char) { +func RemoveInstituteAccess(name *C.char, url *C.char) *C.char { nameStr := C.GoString(name) state, stateErr := GetVPNState(nameStr) if stateErr != nil { @@ -172,7 +183,7 @@ func RemoveInstituteAccess(name *C.char, url *C.char) (*C.char) { } //export RemoveCustomServer -func RemoveCustomServer(name *C.char, url *C.char) (*C.char) { +func RemoveCustomServer(name *C.char, url *C.char) *C.char { nameStr := C.GoString(name) state, stateErr := GetVPNState(nameStr) if stateErr != nil { @@ -218,17 +229,6 @@ func GetConfigCustomServer(name *C.char, url *C.char, forceTCP C.int) (*C.char, return getConfigJSON(config, configType), C.CString(ErrorToString(configErr)) } -//export GetDiscoOrganizations -func GetDiscoOrganizations(name *C.char) (*C.char, *C.char) { - nameStr := C.GoString(name) - state, stateErr := GetVPNState(nameStr) - if stateErr != nil { - return nil, C.CString(ErrorToString(stateErr)) - } - organizations, organizationsErr := state.GetDiscoOrganizations() - return C.CString(organizations), C.CString(ErrorToString(organizationsErr)) -} - //export GetDiscoServers func GetDiscoServers(name *C.char) (*C.char, *C.char) { nameStr := C.GoString(name) diff --git a/exports/servers.go b/exports/servers.go new file mode 100644 index 0000000..f92c08e --- /dev/null +++ b/exports/servers.go @@ -0,0 +1,273 @@ +package main + +/* +// for free +#include <stdlib.h> +#include "c/servers.h" +*/ +import "C" + +import ( + "unsafe" + + "github.com/jwijenbergh/eduvpn-common" + "github.com/jwijenbergh/eduvpn-common/internal/server" +) + +// Get the pointer to the C struct for the profile +// We allocate the struct, the profile ID and the display name +func getCPtrProfile(profile *server.ServerProfile) *C.serverProfile { + // Allocate the struct using malloc and the size of the struct + cProfile := (*C.serverProfile)(C.malloc(C.size_t(unsafe.Sizeof(C.serverProfile{})))) + cProfile.id = C.CString(profile.ID) + cProfile.display_name = C.CString(profile.DisplayName) + if profile.DefaultGateway { + cProfile.default_gateway = C.int(1) + } else { + cProfile.default_gateway = C.int(0) + } + + return cProfile +} + +// Get the pointer to the C struct for the profiles +// We allocate the struct and the struct inside it for the profiles +func getCPtrProfiles(serverProfiles *server.ServerProfileInfo) *C.serverProfiles { + goProfiles := serverProfiles.Info.ProfileList + // Allocate the profles struct using malloc and the size of a pointer + cProfiles := (*C.serverProfiles)(C.malloc(C.size_t(uintptr(0)))) + totalProfiles := C.size_t(len(goProfiles)) + // Defaults if we have no profiles + cProfiles.current = C.int(0) + cProfiles.profiles = nil + cProfiles.total_profiles = totalProfiles + // If we have profiles (which we should), we allocate the struct with malloc and the size of a pointer + // We then fill the struct by converting it to a go slice and get a C pointer for each profile + if totalProfiles > 0 { + profilesPtr := C.malloc(totalProfiles * C.size_t(unsafe.Sizeof(uintptr(0)))) + profiles := (*[1<<30 - 1]*C.serverProfile)(unsafe.Pointer(profilesPtr))[:totalProfiles:totalProfiles] + index := 0 + for _, profile := range goProfiles { + profiles[index] = getCPtrProfile(&profile) + index += 1 + } + // TODO: DO CURRENT PROFILE + cProfiles.profiles = (**C.serverProfile)(profilesPtr) + } + return cProfiles +} + +// Free the profiles by looping through them if there are any +// Also free the pointer itself +func freeCProfiles(profiles *C.serverProfiles) { + // We should only free the profiles if we have them (which we should) + if profiles.total_profiles > 0 { + // Convert it to a go slice + profilesSlice := (*[1<<30 - 1]*C.serverProfile)(unsafe.Pointer(profiles.profiles))[:profiles.total_profiles:profiles.total_profiles] + // Loop through the pointers and free th allocated strings and the struct itself + for i := C.size_t(0); i < profiles.total_profiles; i++ { + C.free(unsafe.Pointer(profilesSlice[i].id)) + C.free(unsafe.Pointer(profilesSlice[i].display_name)) + C.free(unsafe.Pointer(profilesSlice[i])) + } + // Free the inner profiles struct + C.free(unsafe.Pointer(profiles.profiles)) + } + // Free the profiles struct itself + C.free(unsafe.Pointer(profiles)) +} + +// Get a list of strings with a size as a c structure +// Returns the size in size_t and the list of strings as a double pointer char +func getCPtrListStrings(allStrings []string) (C.size_t, **C.char) { + // Get the total strings in size_t + totalStrings := C.size_t(len(allStrings)) + + // If we have strings + // Allocate memory for the strings array + if totalStrings > 0 { + stringsPtr := C.malloc(totalStrings * C.size_t(unsafe.Sizeof(uintptr(0)))) + // Go slice conversion + cStrings := (*[1<<30 - 1]*C.char)(unsafe.Pointer(stringsPtr))[:totalStrings:totalStrings] + + // Loop through and allocate the string for each contact + for index, string := range allStrings { + cStrings[index] = C.CString(string) + } + return totalStrings, (**C.char)(stringsPtr) + } + + // No strings then the length is zero and the char array is nil + return C.size_t(0), nil +} + +// Function for freeing an array/list of strings +// It takes the strings as a pointer to a string and the total strings in size_t +func freeCListStrings(allStrings **C.char, totalStrings C.size_t) { + // If we have strings we should free them + // By converting to a Go slice, and freeing them ony by one + // At last free the pointer itself + if totalStrings > 0 { + stringsSlice := (*[1<<30 - 1]*C.char)(unsafe.Pointer(allStrings))[:totalStrings:totalStrings] + for i := C.size_t(0); i < totalStrings; i++ { + C.free(unsafe.Pointer(stringsSlice[i])) + } + C.free(unsafe.Pointer(allStrings)) + } +} + +// Function for getting the server, +// It gets the main state as a pointer as we need to convert some string maps to localized strings +// It gets the base information for a server as well +func getServer(state *eduvpn.VPNState, base *eduvpn.VPNServerBase) *C.server { + // Allocation using malloc and the size of the struct + server := (*C.server)(C.malloc(C.size_t(unsafe.Sizeof(C.server{})))) + // String allocation and translate the display name + server.identifier = C.CString(base.URL) + server.display_name = C.CString(state.GetTranslated(base.DisplayName)) + // Call the helper to get the list of support contacts + server.total_support_contact, server.support_contact = getCPtrListStrings( + base.SupportContact, + ) + server.profiles = getCPtrProfiles(&base.Profiles) + // No endtime is given if we get servers when it has been partially initialised + if base.EndTime.IsZero() { + server.expire_time = C.ulonglong(0) + } + // The expire time should be stored as an unsigned long long in unix itme + server.expire_time = C.ulonglong(base.EndTime.Unix()) + return server +} + +// Function for freeing a single server +// Gets the pointer to C struct +func freeServer(info *C.server) { + // Free strings + C.free(unsafe.Pointer(info.identifier)) + C.free(unsafe.Pointer(info.display_name)) + + // Free arrays + freeCListStrings(info.support_contact, info.total_support_contact) + freeCProfiles(info.profiles) + + // Free the struct itself + C.free(unsafe.Pointer(info)) +} + +// Get the C ptr to the servers, returns the length in size_t and the double pointer to the struct +func getCPtrServers( + state *eduvpn.VPNState, + serverMap map[string]*server.InstituteAccessServer, +) (C.size_t, **C.server) { + totalServers := C.size_t(len(serverMap)) + // If we have servers, which is not always the case + if totalServers > 0 { + serversPtr := (**C.server)(C.malloc(totalServers * C.size_t(unsafe.Sizeof(uintptr(0))))) + servers := (*[1<<30 - 1]*C.server)(unsafe.Pointer(serversPtr))[:totalServers:totalServers] + index := 0 + for _, server := range serverMap { + cServer := getServer(state, &server.Base) + servers[index] = cServer + index += 1 + } + } + return C.size_t(0), nil +} + +//export FreeServers +// This function takes the servers as a C struct pointer as input +// It frees all allocated memory for the server +func FreeServers(cServers *C.servers) { + // Free the custom servers if there are any + if cServers.total_custom > 0 { + customServers := (*[1<<30 - 1]*C.server)(unsafe.Pointer(cServers.custom_servers))[:cServers.total_custom:cServers.total_custom] + for i := C.size_t(0); i < cServers.total_custom; i++ { + freeServer(customServers[i]) + } + C.free(unsafe.Pointer(cServers.custom_servers)) + } + // Free the institute access servers if there are any + if cServers.total_institute > 0 { + instituteServers := (*[1<<30 - 1]*C.server)(unsafe.Pointer(cServers.institute_servers))[:cServers.total_institute:cServers.total_institute] + + for i := C.size_t(0); i < cServers.total_institute; i++ { + freeServer(instituteServers[i]) + } + C.free(unsafe.Pointer(cServers.institute_servers)) + } + // Free the secure internet server if there is one + if cServers.secure_internet_server != nil { + C.free(unsafe.Pointer(cServers.secure_internet_server.country_code)) + freeServer(cServers.secure_internet_server) + } + // Free the structure itself + C.free(unsafe.Pointer(cServers)) +} + +// Return the servers as a C struct pointer +// It takes the state as a pointer as we need to translate some strings +// It also takes the servers as a pointer that belongs to the main state or gathered from the callback +func getSavedServersWithOptions(state *eduvpn.VPNState, servers *server.Servers) *C.servers { + // Allocate the struct that we will return + // With the size of the c struct + returnedStruct := (*C.servers)(C.malloc(C.size_t(unsafe.Sizeof(C.servers{})))) + + // Get the different categories of servers + totalCustom, customPtr := getCPtrServers(state, servers.CustomServers.Map) + totalInstitute, institutePtr := getCPtrServers(state, servers.InstituteServers.Map) + var secureServerPtr *C.server = nil + secureInternetBase, secureInternetBaseErr := servers.SecureInternetHomeServer.GetBase() + if secureInternetBaseErr == nil && secureInternetBase != nil { + // FIXME: log error? + secureServerPtr = getServer(state, secureInternetBase) + // Give a new identifier + C.free(unsafe.Pointer(secureServerPtr.identifier)) + secureServerPtr.identifier = C.CString(servers.SecureInternetHomeServer.HomeOrganizationID) + secureServerPtr.country_code = C.CString(servers.SecureInternetHomeServer.CurrentLocation) + } + + // Fill the struct and return + returnedStruct.custom_servers = customPtr + returnedStruct.total_custom = totalCustom + returnedStruct.institute_servers = institutePtr + returnedStruct.total_institute = totalInstitute + returnedStruct.secure_internet_server = secureServerPtr + return returnedStruct +} + +//export GetSavedServers +// This function takes the name as input which is the name of the client +// It gets the state by name and then returns the saved servers as a c struct belonging to it +func GetSavedServers(name *C.char) *C.servers { + nameStr := C.GoString(name) + state, stateErr := GetVPNState(nameStr) + if stateErr != nil { + // TODO: Remove this panic + panic(stateErr) + } + return getSavedServersWithOptions(state, &state.Servers) +} + +// This function takes the state as input which is the main state +// It also takes the data as an interface and if it has the servers type gets the data as a c struct otherwise nil +func getTransitionDataServers(state *eduvpn.VPNState, data interface{}) *C.servers { + if converted, ok := data.(server.Servers); ok { + return getSavedServersWithOptions(state, &converted) + } + return nil +} + +//export FreeSecureLocations +func FreeSecureLocations(locations *C.serverLocations) { + freeCListStrings(locations.locations, locations.total_locations); + C.free(unsafe.Pointer(locations)) +} + +func getTransitionSecureLocations(data interface{}) (*C.serverLocations) { + if locations, ok := data.([]string); ok { + returnedStruct := (*C.serverLocations)(C.malloc(C.size_t(unsafe.Sizeof(C.servers{})))) + returnedStruct.total_locations, returnedStruct.locations = getCPtrListStrings(locations) + return returnedStruct + } + return nil +} |
